@zenera/cli 1.1.24 → 1.1.25
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 +53 -0
- package/dist/ask.js +28 -5
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +6 -0
- package/dist/batch.js +76 -1
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +80 -7
- package/dist/commands/inspect.d.ts +47 -1
- package/dist/commands/inspect.js +167 -18
- package/dist/commands/memory.js +128 -29
- package/dist/commands/meta.js +311 -21
- package/dist/commands/models.js +96 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +76 -14
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +1 -0
- package/dist/eventlog.d.ts +59 -0
- package/dist/eventlog.js +146 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +15 -1
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +60 -0
- package/dist/meta/finetune/children.d.ts +8 -0
- package/dist/meta/finetune/children.js +76 -0
- package/dist/meta/finetune/command.d.ts +30 -0
- package/dist/meta/finetune/command.js +338 -0
- package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
- package/dist/meta/finetune/dataset/anchors.js +239 -0
- package/dist/meta/finetune/dataset/changes.d.ts +36 -0
- package/dist/meta/finetune/dataset/changes.js +319 -0
- package/dist/meta/finetune/dataset/command.d.ts +17 -0
- package/dist/meta/finetune/dataset/command.js +1011 -0
- package/dist/meta/finetune/dataset/drift.d.ts +33 -0
- package/dist/meta/finetune/dataset/drift.js +71 -0
- package/dist/meta/finetune/dataset/export.d.ts +26 -0
- package/dist/meta/finetune/dataset/export.js +51 -0
- package/dist/meta/finetune/dataset/index.d.ts +2 -0
- package/dist/meta/finetune/dataset/index.js +2 -0
- package/dist/meta/finetune/dataset/sample.d.ts +50 -0
- package/dist/meta/finetune/dataset/sample.js +197 -0
- package/dist/meta/finetune/dataset/schema.d.ts +33 -0
- package/dist/meta/finetune/dataset/schema.js +216 -0
- package/dist/meta/finetune/dataset/store.d.ts +35 -0
- package/dist/meta/finetune/dataset/store.js +177 -0
- package/dist/meta/finetune/dataset/types.d.ts +125 -0
- package/dist/meta/finetune/dataset/types.js +39 -0
- package/dist/meta/finetune/failure.d.ts +15 -0
- package/dist/meta/finetune/failure.js +26 -0
- package/dist/meta/finetune/feedback.d.ts +45 -0
- package/dist/meta/finetune/feedback.js +85 -0
- package/dist/meta/finetune/improvements.d.ts +51 -0
- package/dist/meta/finetune/improvements.js +244 -0
- package/dist/meta/finetune/index.d.ts +2 -0
- package/dist/meta/finetune/index.js +2 -0
- package/dist/meta/finetune/journal.d.ts +35 -0
- package/dist/meta/finetune/journal.js +135 -0
- package/dist/meta/finetune/live.d.ts +74 -0
- package/dist/meta/finetune/live.js +226 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +114 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/report.d.ts +33 -0
- package/dist/meta/finetune/report.js +1284 -0
- package/dist/meta/finetune/retry.d.ts +24 -0
- package/dist/meta/finetune/retry.js +96 -0
- package/dist/meta/finetune/sampler.d.ts +18 -0
- package/dist/meta/finetune/sampler.js +41 -0
- package/dist/meta/finetune/system.d.ts +27 -0
- package/dist/meta/finetune/system.js +132 -0
- package/dist/meta/finetune/train.d.ts +39 -0
- package/dist/meta/finetune/train.js +391 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +291 -0
- package/dist/meta/finetune/tuning.d.ts +167 -0
- package/dist/meta/finetune/tuning.js +289 -0
- package/dist/meta/finetune/usage.d.ts +62 -0
- package/dist/meta/finetune/usage.js +225 -0
- package/dist/meta/host.d.ts +14 -0
- package/dist/meta/host.js +20 -0
- package/dist/meta/index.d.ts +11 -0
- package/dist/meta/index.js +11 -0
- package/dist/meta/launch.d.ts +81 -0
- package/dist/meta/launch.js +286 -0
- package/dist/meta/model.d.ts +44 -0
- package/dist/meta/model.js +119 -0
- package/dist/meta/prompts.d.ts +18 -0
- package/dist/meta/prompts.js +61 -0
- package/dist/meta/relay.d.ts +20 -0
- package/dist/meta/relay.js +123 -0
- package/dist/meta/tokens.d.ts +52 -0
- package/dist/meta/tokens.js +226 -0
- package/dist/meta/window.d.ts +61 -0
- package/dist/meta/window.js +198 -0
- package/dist/meta/wire.d.ts +42 -0
- package/dist/meta/wire.js +129 -0
- package/dist/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +26 -0
- package/dist/podman.js +83 -3
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -6
- package/dist/scaffold.js +5 -60
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.js +53 -2
- package/dist/tui/models.d.ts +33 -0
- package/dist/tui/models.js +389 -0
- package/dist/tui/wrap.d.ts +5 -0
- package/dist/tui/wrap.js +12 -0
- package/dist/usage.d.ts +78 -0
- package/dist/usage.js +51 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +31 -102
- package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
- package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
- package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
- package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +47 -9
- package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
- package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
- package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1449 -658
- package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +60 -25
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +18 -14
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +7 -3
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +29 -20
- package/templates/project/agents/memory-instructions.md +31 -32
- package/templates/project/agents/tools-instructions.md +7 -4
- package/templates/project/gitignore +2 -0
- package/dist/meta.d.ts +0 -221
- package/dist/meta.js +0 -684
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
import { anyOf, loose, matcher, PatternError } from '@zenera/neo';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { bold, cyan, dim, green, invalidError, json, note, parse, red, table, usageError, write, writeAll, yellow, } from "../../host.js";
|
|
5
|
+
import { sessionKept } from "../../tokens.js";
|
|
6
|
+
import { planApply, planEdit, planIgnored, planStatus } from "./changes.js";
|
|
7
|
+
import { drift } from "./drift.js";
|
|
8
|
+
import { batchInput, dump } from "./export.js";
|
|
9
|
+
import { lastVerdict, matches, ORDER_KEYS, sample, STRATA_FIELDS, } from "./sample.js";
|
|
10
|
+
import { readAnchor, readProposal } from "./schema.js";
|
|
11
|
+
import { DatasetStore, whoIsThis } from "./store.js";
|
|
12
|
+
import { anchorText, COMPLEXITIES, NOTE_KINDS, sameAnchor, VERDICTS, } from "./types.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// zen meta dataset
|
|
15
|
+
//
|
|
16
|
+
// The only writer of `dataset/`. An extraction is the model's work and stays
|
|
17
|
+
// in the prompt; everything after it — what changed, which revision, who did it,
|
|
18
|
+
// what drifted, what to run next — is bookkeeping, and bookkeeping a model half
|
|
19
|
+
// does is worse than none. So it is all here, and the prompt calls it.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
export const DATASET_USAGE = 'zen meta dataset <verb> [args] [options]';
|
|
22
|
+
export const DATASET_HELP = [
|
|
23
|
+
'Dataset:',
|
|
24
|
+
' zen meta dataset [status] revision, counts, drift',
|
|
25
|
+
' zen meta dataset ls [filters] the cases, one row each',
|
|
26
|
+
' zen meta dataset show <id[@rev]>... cases in full, now or at a revision',
|
|
27
|
+
' zen meta dataset log [id...] changes and notes, with their sessions',
|
|
28
|
+
' zen meta dataset drift [--all] which cases their sources moved under',
|
|
29
|
+
' zen meta dataset apply <file> --why <w> write a proposal (--partial, --dry-run)',
|
|
30
|
+
' zen meta dataset update <id> --why <w> --set k=v, --rubric-add/-edit/-drop, --tag-add/-drop',
|
|
31
|
+
' zen meta dataset retire|restore <id>... take a case out of use, or back',
|
|
32
|
+
' zen meta dataset reanchor <id> --why <w> --file, --heading or --pointer',
|
|
33
|
+
' zen meta dataset ignore <file> --why <w> a section that holds no case (--drop to undo)',
|
|
34
|
+
' zen meta dataset note <id>... -m <text> --kind, --run, --verdict, --rubric r1=pass,...',
|
|
35
|
+
' zen meta dataset sample [filters] draw cases: -n, --seed, --by, --weight, --order',
|
|
36
|
+
' zen meta dataset export [id...] cases out: --format cases|batch, -o <file>',
|
|
37
|
+
'',
|
|
38
|
+
'Filters (ls, sample, export): --class, --complexity, --tag, --status, --rubric yes|no,',
|
|
39
|
+
' --expected yes|no, --id <glob>, --exclude <glob>, --ids-from <file>, --at <rev>,',
|
|
40
|
+
' --changed-since <rev>, --restarted, --verdict, --never-graded, --note <kind>,',
|
|
41
|
+
' --graded-in <run>, --source <glob>, --anchor <glob>, --grep <text> (--regex, --case-sensitive)',
|
|
42
|
+
];
|
|
43
|
+
export const DATASET_SUMMARY = 'The cases the project is evaluated on: read from sources, revised, sampled.';
|
|
44
|
+
/** The page `zen meta dataset --help` prints. */
|
|
45
|
+
export const DATASET_DETAILS = [
|
|
46
|
+
'Verbs:',
|
|
47
|
+
...DATASET_HELP.slice(1),
|
|
48
|
+
'',
|
|
49
|
+
'Sampling (sample): --by <class,complexity,tag,source,rubric,verdict|none> groups the',
|
|
50
|
+
' cases (default class), --order <rubric,complexity,verdict,rev,id> ranks inside a',
|
|
51
|
+
' group (default rubric,complexity), --weight <group>=<w> gives a group more turns,',
|
|
52
|
+
' -n <count>, --seed <n>. A larger -n keeps every case a smaller one chose.',
|
|
53
|
+
'',
|
|
54
|
+
'Output (sample, export): --format ids|batch|cases|json, -o <file>. batch is',
|
|
55
|
+
' zen run batch input and never holds a rubric.',
|
|
56
|
+
'',
|
|
57
|
+
'Every write needs --why and is a new revision; only a change to input, rubric',
|
|
58
|
+
'or expected restarts a case. Only this command writes dataset/. Build or refresh',
|
|
59
|
+
'it from its sources with: zen meta run /dataset',
|
|
60
|
+
'',
|
|
61
|
+
'Examples:',
|
|
62
|
+
' zen meta dataset',
|
|
63
|
+
' zen meta dataset drift',
|
|
64
|
+
' zen meta dataset sample --rubric yes -n 12 --format batch -o cases.json',
|
|
65
|
+
' zen meta dataset log plan-day',
|
|
66
|
+
];
|
|
67
|
+
const OPTIONS = {
|
|
68
|
+
project: { type: 'string' },
|
|
69
|
+
why: { type: 'string' },
|
|
70
|
+
partial: { type: 'boolean' },
|
|
71
|
+
'dry-run': { type: 'boolean' },
|
|
72
|
+
all: { type: 'boolean' },
|
|
73
|
+
set: { type: 'string', multiple: true },
|
|
74
|
+
'rubric-add': { type: 'string', multiple: true },
|
|
75
|
+
'rubric-edit': { type: 'string', multiple: true },
|
|
76
|
+
'rubric-drop': { type: 'string', multiple: true },
|
|
77
|
+
'tag-add': { type: 'string', multiple: true },
|
|
78
|
+
'tag-drop': { type: 'string', multiple: true },
|
|
79
|
+
override: { type: 'boolean' },
|
|
80
|
+
file: { type: 'string' },
|
|
81
|
+
heading: { type: 'string' },
|
|
82
|
+
pointer: { type: 'string' },
|
|
83
|
+
drop: { type: 'boolean' },
|
|
84
|
+
message: { type: 'string', short: 'm' },
|
|
85
|
+
kind: { type: 'string' },
|
|
86
|
+
run: { type: 'string' },
|
|
87
|
+
verdict: { type: 'string', multiple: true },
|
|
88
|
+
rubric: { type: 'string' },
|
|
89
|
+
class: { type: 'string', multiple: true },
|
|
90
|
+
complexity: { type: 'string', multiple: true },
|
|
91
|
+
tag: { type: 'string', multiple: true },
|
|
92
|
+
status: { type: 'string' },
|
|
93
|
+
expected: { type: 'string' },
|
|
94
|
+
id: { type: 'string', multiple: true },
|
|
95
|
+
exclude: { type: 'string', multiple: true },
|
|
96
|
+
'ids-from': { type: 'string' },
|
|
97
|
+
at: { type: 'string' },
|
|
98
|
+
'changed-since': { type: 'string' },
|
|
99
|
+
restarted: { type: 'boolean' },
|
|
100
|
+
'never-graded': { type: 'boolean' },
|
|
101
|
+
note: { type: 'string', multiple: true },
|
|
102
|
+
'graded-in': { type: 'string', multiple: true },
|
|
103
|
+
source: { type: 'string', multiple: true },
|
|
104
|
+
anchor: { type: 'string', multiple: true },
|
|
105
|
+
grep: { type: 'string' },
|
|
106
|
+
regex: { type: 'boolean' },
|
|
107
|
+
'case-sensitive': { type: 'boolean' },
|
|
108
|
+
by: { type: 'string' },
|
|
109
|
+
weight: { type: 'string', multiple: true },
|
|
110
|
+
order: { type: 'string' },
|
|
111
|
+
count: { type: 'string', short: 'n' },
|
|
112
|
+
seed: { type: 'string' },
|
|
113
|
+
format: { type: 'string' },
|
|
114
|
+
out: { type: 'string', short: 'o' },
|
|
115
|
+
quiet: { type: 'boolean' },
|
|
116
|
+
};
|
|
117
|
+
const VERBS = [
|
|
118
|
+
'status',
|
|
119
|
+
'ls',
|
|
120
|
+
'show',
|
|
121
|
+
'log',
|
|
122
|
+
'drift',
|
|
123
|
+
'apply',
|
|
124
|
+
'update',
|
|
125
|
+
'retire',
|
|
126
|
+
'restore',
|
|
127
|
+
'reanchor',
|
|
128
|
+
'ignore',
|
|
129
|
+
'note',
|
|
130
|
+
'sample',
|
|
131
|
+
'export',
|
|
132
|
+
];
|
|
133
|
+
/** Only the project flag, read ahead of resolving the project the rest is about. */
|
|
134
|
+
export function datasetProjectFlag(args) {
|
|
135
|
+
return parse(args, OPTIONS, DATASET_USAGE).values.project;
|
|
136
|
+
}
|
|
137
|
+
export async function runDataset(ctx, project) {
|
|
138
|
+
const { values, positionals } = parse(ctx.args, OPTIONS, DATASET_USAGE);
|
|
139
|
+
const [verb = 'status', ...rest] = positionals;
|
|
140
|
+
if (!VERBS.includes(verb)) {
|
|
141
|
+
throw usageError(`unknown dataset verb: ${verb}`, `one of: ${VERBS.join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
const run = { ctx, values, rest, root: project.dir, project: project.name };
|
|
144
|
+
switch (verb) {
|
|
145
|
+
case 'status':
|
|
146
|
+
return status(run);
|
|
147
|
+
case 'ls':
|
|
148
|
+
return list(run);
|
|
149
|
+
case 'show':
|
|
150
|
+
return show(run);
|
|
151
|
+
case 'log':
|
|
152
|
+
return log(run);
|
|
153
|
+
case 'drift':
|
|
154
|
+
return driftCommand(run);
|
|
155
|
+
case 'apply':
|
|
156
|
+
return apply(run);
|
|
157
|
+
case 'update':
|
|
158
|
+
return update(run);
|
|
159
|
+
case 'retire':
|
|
160
|
+
case 'restore':
|
|
161
|
+
return setStatus(run, verb === 'retire' ? 'retired' : 'active');
|
|
162
|
+
case 'reanchor':
|
|
163
|
+
return reanchor(run);
|
|
164
|
+
case 'ignore':
|
|
165
|
+
return ignore(run);
|
|
166
|
+
case 'note':
|
|
167
|
+
return addNote(run);
|
|
168
|
+
case 'sample':
|
|
169
|
+
return draw(run, 'sample');
|
|
170
|
+
case 'export':
|
|
171
|
+
return draw(run, 'export');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Small readers
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
function whole(text, flag, min = 0) {
|
|
178
|
+
if (text === undefined) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const n = Number(text);
|
|
182
|
+
if (!Number.isInteger(n) || n < min) {
|
|
183
|
+
throw usageError(`${flag} takes a whole number of at least ${min}`, `got "${text}"`);
|
|
184
|
+
}
|
|
185
|
+
return n;
|
|
186
|
+
}
|
|
187
|
+
function yesNo(text, flag) {
|
|
188
|
+
if (text === undefined) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
if (text !== 'yes' && text !== 'no') {
|
|
192
|
+
throw usageError(`${flag} takes yes or no`, `got "${text}"`);
|
|
193
|
+
}
|
|
194
|
+
return text === 'yes';
|
|
195
|
+
}
|
|
196
|
+
function oneOf(values, known, flag) {
|
|
197
|
+
if (!values || values.length === 0) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
const wrong = values.filter((v) => !known.includes(v));
|
|
201
|
+
if (wrong.length > 0) {
|
|
202
|
+
throw usageError(`${flag} ${wrong.map((w) => `"${w}"`).join(', ')} names nothing in this dataset`, `known: ${[...known].sort().join(', ') || '(none)'}`);
|
|
203
|
+
}
|
|
204
|
+
return values;
|
|
205
|
+
}
|
|
206
|
+
function patterns(values, flag, regex = false, caseSensitive = false) {
|
|
207
|
+
try {
|
|
208
|
+
return anyOf((values ?? []).map((p) => loose(p, { regex, caseSensitive })));
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (err instanceof PatternError || err instanceof SyntaxError) {
|
|
212
|
+
throw usageError(`${flag}: ${err.message}`);
|
|
213
|
+
}
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function needWhy(values) {
|
|
218
|
+
const why = values.why?.trim();
|
|
219
|
+
if (!why) {
|
|
220
|
+
throw usageError('a write needs --why <reason>', 'say what changed in the source, or why by hand');
|
|
221
|
+
}
|
|
222
|
+
return why;
|
|
223
|
+
}
|
|
224
|
+
function readJsonFile(file) {
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
throw invalidError(`${file}: ${err.message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Opens for writing, runs, and always lets go. */
|
|
233
|
+
function writing(root, fn) {
|
|
234
|
+
const store = DatasetStore.open(root, { write: true });
|
|
235
|
+
try {
|
|
236
|
+
return fn(store);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
store.release();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function requireDataset(store) {
|
|
243
|
+
if (!store.exists()) {
|
|
244
|
+
throw invalidError(`no dataset in ${store.dir}`, 'build one: zen meta run /dataset');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const notesOf = (store) => {
|
|
248
|
+
const cache = new Map();
|
|
249
|
+
return (c) => {
|
|
250
|
+
let found = cache.get(c.id);
|
|
251
|
+
if (!found) {
|
|
252
|
+
found = store.journal(c.id).filter((r) => r.type === 'note');
|
|
253
|
+
cache.set(c.id, found);
|
|
254
|
+
}
|
|
255
|
+
return found;
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
/** The cases as they stand, or as they stood at `--at`. */
|
|
259
|
+
function casesAt(store, at) {
|
|
260
|
+
if (at === undefined) {
|
|
261
|
+
return store.all();
|
|
262
|
+
}
|
|
263
|
+
if (at > store.manifest.revision) {
|
|
264
|
+
throw usageError(`--at ${at} is past the latest revision, ${store.manifest.revision}`);
|
|
265
|
+
}
|
|
266
|
+
return store.ids().flatMap((id) => store.at(id, at) ?? []);
|
|
267
|
+
}
|
|
268
|
+
function inputText(c) {
|
|
269
|
+
if (typeof c.input === 'string') {
|
|
270
|
+
return c.input;
|
|
271
|
+
}
|
|
272
|
+
return c.input
|
|
273
|
+
.map((p) => (typeof p === 'string' ? p : 'text' in p ? p.text : `[${Object.keys(p)[0]}]`))
|
|
274
|
+
.join(' ');
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Criteria, shared by ls / sample / export
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
function criteriaOf(run, store, cases, at) {
|
|
280
|
+
const v = run.values;
|
|
281
|
+
const status = v.status ?? 'active';
|
|
282
|
+
if (!['active', 'retired', 'all'].includes(status)) {
|
|
283
|
+
throw usageError('--status takes active, retired or all', `got "${status}"`);
|
|
284
|
+
}
|
|
285
|
+
const classes = new Set(cases.map((c) => c.class ?? 'unclassified'));
|
|
286
|
+
const tags = new Set(cases.flatMap((c) => c.tags ?? []));
|
|
287
|
+
let idsFrom;
|
|
288
|
+
if (v['ids-from']) {
|
|
289
|
+
idsFrom = idsIn(readFileIds(resolve(run.ctx.cwd, v['ids-from'])));
|
|
290
|
+
}
|
|
291
|
+
let restarted;
|
|
292
|
+
if (v.restarted) {
|
|
293
|
+
const rev = at ?? store.manifest.revision;
|
|
294
|
+
restarted = new Set(store.revisions().find((r) => r.rev === rev)?.restarted ?? []);
|
|
295
|
+
}
|
|
296
|
+
const kinds = oneOf(v.note, NOTE_KINDS, '--note');
|
|
297
|
+
return {
|
|
298
|
+
status: status,
|
|
299
|
+
classes: oneOf(v.class, [...classes], '--class'),
|
|
300
|
+
complexities: oneOf(v.complexity, [...COMPLEXITIES, 'unrated'], '--complexity'),
|
|
301
|
+
tags: oneOf(v.tag, [...tags], '--tag'),
|
|
302
|
+
rubric: yesNo(v.rubric, '--rubric'),
|
|
303
|
+
expected: yesNo(v.expected, '--expected'),
|
|
304
|
+
ids: patterns(v.id, '--id'),
|
|
305
|
+
exclude: patterns(v.exclude, '--exclude'),
|
|
306
|
+
...(idsFrom ? { idsFrom } : {}),
|
|
307
|
+
changedSince: whole(v['changed-since'], '--changed-since'),
|
|
308
|
+
...(restarted ? { restarted } : {}),
|
|
309
|
+
verdicts: oneOf(v.verdict, VERDICTS, '--verdict'),
|
|
310
|
+
neverGraded: v['never-graded'],
|
|
311
|
+
noteKinds: kinds,
|
|
312
|
+
gradedIn: v['graded-in'],
|
|
313
|
+
source: patterns(v.source, '--source'),
|
|
314
|
+
anchor: patterns(v.anchor, '--anchor'),
|
|
315
|
+
grep: v.grep
|
|
316
|
+
? (() => {
|
|
317
|
+
try {
|
|
318
|
+
return matcher(v.grep, {
|
|
319
|
+
regex: v.regex,
|
|
320
|
+
caseSensitive: v['case-sensitive'],
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
throw usageError(`--grep: ${err.message}`);
|
|
325
|
+
}
|
|
326
|
+
})()
|
|
327
|
+
: undefined,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function readFileIds(file) {
|
|
331
|
+
const text = (() => {
|
|
332
|
+
try {
|
|
333
|
+
return readFileSync(file, 'utf8');
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
throw usageError(`--ids-from: ${err.message}`);
|
|
337
|
+
}
|
|
338
|
+
})();
|
|
339
|
+
try {
|
|
340
|
+
return JSON.parse(text);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return text.split(/\s+/).filter(Boolean);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** A list of ids, or any of the shapes that carry them: a sample, a batch, a dump. */
|
|
347
|
+
function idsIn(doc) {
|
|
348
|
+
const list = Array.isArray(doc)
|
|
349
|
+
? doc
|
|
350
|
+
: doc && typeof doc === 'object'
|
|
351
|
+
? (doc.ids ??
|
|
352
|
+
doc.batch ??
|
|
353
|
+
doc.cases)
|
|
354
|
+
: undefined;
|
|
355
|
+
if (!Array.isArray(list)) {
|
|
356
|
+
throw usageError('--ids-from names no ids', 'a list of ids, or json with ids, batch or cases');
|
|
357
|
+
}
|
|
358
|
+
return new Set(list.flatMap((x) => typeof x === 'string'
|
|
359
|
+
? [x]
|
|
360
|
+
: x && typeof x === 'object' && 'id' in x
|
|
361
|
+
? [String(x.id)]
|
|
362
|
+
: []));
|
|
363
|
+
}
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// status / ls / show / log
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
function count(rows, key) {
|
|
368
|
+
const out = {};
|
|
369
|
+
for (const c of rows) {
|
|
370
|
+
out[key(c)] = (out[key(c)] ?? 0) + 1;
|
|
371
|
+
}
|
|
372
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
373
|
+
}
|
|
374
|
+
function status(run) {
|
|
375
|
+
const store = DatasetStore.open(run.root);
|
|
376
|
+
if (!store.exists()) {
|
|
377
|
+
if (run.ctx.json) {
|
|
378
|
+
return json({ dir: store.dir, exists: false });
|
|
379
|
+
}
|
|
380
|
+
note(`no dataset yet in ${store.dir}`);
|
|
381
|
+
note(dim('build one: zen meta run /dataset'));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const all = store.all();
|
|
385
|
+
const active = all.filter((c) => c.status === 'active');
|
|
386
|
+
const report = drift(store);
|
|
387
|
+
const unfinished = store.unfinished();
|
|
388
|
+
const summary = {
|
|
389
|
+
dir: store.dir,
|
|
390
|
+
exists: true,
|
|
391
|
+
revision: store.manifest.revision,
|
|
392
|
+
cases: { active: active.length, retired: all.length - active.length },
|
|
393
|
+
classes: count(active, (c) => c.class ?? 'unclassified'),
|
|
394
|
+
complexity: count(active, (c) => c.complexity ?? 'unrated'),
|
|
395
|
+
rubric: active.filter((c) => c.rubric.length > 0).length,
|
|
396
|
+
sources: new Set([
|
|
397
|
+
...active.flatMap((c) => (c.source ? [c.source.file] : [])),
|
|
398
|
+
...store.manifest.ignored.map((i) => i.file),
|
|
399
|
+
]).size,
|
|
400
|
+
ignored: store.manifest.ignored.length,
|
|
401
|
+
drift: {
|
|
402
|
+
clean: report.clean,
|
|
403
|
+
changed: report.cases.filter((c) => c.state === 'changed').length,
|
|
404
|
+
gone: report.cases.filter((c) => c.state === 'gone').length,
|
|
405
|
+
uncovered: report.uncovered.length,
|
|
406
|
+
},
|
|
407
|
+
unfinished,
|
|
408
|
+
};
|
|
409
|
+
if (run.ctx.json) {
|
|
410
|
+
return json(summary);
|
|
411
|
+
}
|
|
412
|
+
write(`${bold('revision')} ${summary.revision}`);
|
|
413
|
+
write(`${bold('cases')} ${active.length} active, ${summary.cases.retired} retired, ${summary.rubric} with a rubric`);
|
|
414
|
+
write(`${bold('classes')} ${Object.entries(summary.classes)
|
|
415
|
+
.map(([k, n]) => `${k} ${n}`)
|
|
416
|
+
.join(' · ')}`);
|
|
417
|
+
write(`${bold('complex')} ${Object.entries(summary.complexity)
|
|
418
|
+
.map(([k, n]) => `${k} ${n}`)
|
|
419
|
+
.join(' · ')}`);
|
|
420
|
+
write(`${bold('sources')} ${summary.sources} file(s), ${summary.ignored} section(s) ignored`);
|
|
421
|
+
write(`${bold('drift')} ${report.clean
|
|
422
|
+
? green('clean')
|
|
423
|
+
: yellow(`${summary.drift.changed} changed, ${summary.drift.gone} gone, ${summary.drift.uncovered} uncovered — zen meta dataset drift`)}`);
|
|
424
|
+
if (unfinished.length > 0) {
|
|
425
|
+
write(red(`unfinished ${unfinished.length} case(s) written past the manifest: an apply was interrupted — apply it again`));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function list(run) {
|
|
429
|
+
const store = DatasetStore.open(run.root);
|
|
430
|
+
requireDataset(store);
|
|
431
|
+
const at = whole(run.values.at, '--at', 1);
|
|
432
|
+
const cases = casesAt(store, at);
|
|
433
|
+
const notes = notesOf(store);
|
|
434
|
+
const criteria = criteriaOf(run, store, cases, at);
|
|
435
|
+
const shown = cases.filter((c) => matches(c, criteria, notes));
|
|
436
|
+
if (run.ctx.json) {
|
|
437
|
+
return json(shown);
|
|
438
|
+
}
|
|
439
|
+
if (shown.length === 0) {
|
|
440
|
+
note('no case matches');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
writeAll(table([
|
|
444
|
+
['ID', 'REV', 'CLASS', 'COMPLEXITY', 'RUBRIC', 'VERDICT', 'TAGS'].map((h) => bold(h)),
|
|
445
|
+
...shown.map((c) => [
|
|
446
|
+
c.status === 'retired' ? dim(c.id) : cyan(c.id),
|
|
447
|
+
String(c.rev),
|
|
448
|
+
c.class ?? dim('-'),
|
|
449
|
+
c.complexity ?? dim('-'),
|
|
450
|
+
String(c.rubric.length),
|
|
451
|
+
lastVerdict(c, notes) ?? dim('-'),
|
|
452
|
+
(c.tags ?? []).join(',') || dim('-'),
|
|
453
|
+
]),
|
|
454
|
+
]));
|
|
455
|
+
note(dim(`${shown.length} of ${cases.length} case(s)`));
|
|
456
|
+
}
|
|
457
|
+
function target(text) {
|
|
458
|
+
const at = text.lastIndexOf('@');
|
|
459
|
+
if (at <= 0) {
|
|
460
|
+
return { id: text };
|
|
461
|
+
}
|
|
462
|
+
const rev = whole(text.slice(at + 1), `${text}: the revision`, 1);
|
|
463
|
+
return { id: text.slice(0, at), rev };
|
|
464
|
+
}
|
|
465
|
+
function show(run) {
|
|
466
|
+
if (run.rest.length === 0) {
|
|
467
|
+
throw usageError('show which case?', 'zen meta dataset show <id>[@rev]');
|
|
468
|
+
}
|
|
469
|
+
const store = DatasetStore.open(run.root);
|
|
470
|
+
requireDataset(store);
|
|
471
|
+
const found = [];
|
|
472
|
+
const missing = [];
|
|
473
|
+
for (const text of run.rest) {
|
|
474
|
+
const { id, rev } = target(text);
|
|
475
|
+
const c = rev === undefined ? store.get(id) : store.at(id, rev);
|
|
476
|
+
if (c) {
|
|
477
|
+
found.push(c);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
missing.push(text);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (missing.length > 0) {
|
|
484
|
+
throw invalidError(`no such case: ${missing.join(', ')}`, 'see: zen meta dataset ls --status all');
|
|
485
|
+
}
|
|
486
|
+
if (run.ctx.json) {
|
|
487
|
+
return json(found);
|
|
488
|
+
}
|
|
489
|
+
found.forEach((c, i) => {
|
|
490
|
+
if (i > 0) {
|
|
491
|
+
write('');
|
|
492
|
+
}
|
|
493
|
+
const facets = [c.class, c.complexity, ...(c.tags ?? [])].filter(Boolean).join(' · ');
|
|
494
|
+
write(`${bold(cyan(c.id))} rev ${c.rev} ${c.status === 'active' ? c.status : yellow(c.status)} ${dim(facets)}`);
|
|
495
|
+
if (c.source) {
|
|
496
|
+
write(`${dim('source')} ${c.source.file} ${dim(anchorText(c.source.anchor))}`);
|
|
497
|
+
}
|
|
498
|
+
write(`${dim('input')} ${inputText(c)}`);
|
|
499
|
+
if (c.rubric.length > 0) {
|
|
500
|
+
write(dim('rubric'));
|
|
501
|
+
for (const r of c.rubric) {
|
|
502
|
+
write(` ${dim(r.id)} ${r.text}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (c.expected) {
|
|
506
|
+
write(`${dim('expected')} ${c.expected}`);
|
|
507
|
+
}
|
|
508
|
+
if (c.notes) {
|
|
509
|
+
write(`${dim('notes')} ${c.notes}`);
|
|
510
|
+
}
|
|
511
|
+
if (c.overrides?.length) {
|
|
512
|
+
write(`${dim('overrides')} ${c.overrides.join(', ')}`);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
function who(by) {
|
|
517
|
+
return [by.prompt ? `/${by.prompt}` : '', by.session ? by.session.slice(0, 8) : '']
|
|
518
|
+
.filter(Boolean)
|
|
519
|
+
.join(' ');
|
|
520
|
+
}
|
|
521
|
+
function log(run) {
|
|
522
|
+
const store = DatasetStore.open(run.root);
|
|
523
|
+
requireDataset(store);
|
|
524
|
+
const ids = run.rest.length > 0 ? run.rest : store.ids();
|
|
525
|
+
const unknown = ids.filter((id) => !store.get(id));
|
|
526
|
+
if (unknown.length > 0) {
|
|
527
|
+
throw invalidError(`no such case: ${unknown.join(', ')}`);
|
|
528
|
+
}
|
|
529
|
+
const rows = ids
|
|
530
|
+
.flatMap((id) => store.journal(id).map((row) => {
|
|
531
|
+
const { case: _case, ...rest } = row;
|
|
532
|
+
return { id, ...rest };
|
|
533
|
+
}))
|
|
534
|
+
.sort((a, b) => a.at.localeCompare(b.at) || a.id.localeCompare(b.id));
|
|
535
|
+
if (run.ctx.json) {
|
|
536
|
+
return json(rows);
|
|
537
|
+
}
|
|
538
|
+
for (const row of rows) {
|
|
539
|
+
const when = dim(row.at.slice(0, 16).replace('T', ' '));
|
|
540
|
+
const by = dim(who(row.by));
|
|
541
|
+
if (row.type === 'change') {
|
|
542
|
+
const fields = row.changes.map((c) => c.path).join(', ');
|
|
543
|
+
write(`${when} ${cyan(row.id)} rev ${row.rev} ${bold(row.op)}${fields ? ` ${fields}` : ''} ${dim(`"${row.why}"`)} ${by}`);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
const verdict = row.verdict
|
|
547
|
+
? row.verdict === 'right'
|
|
548
|
+
? green(row.verdict)
|
|
549
|
+
: row.verdict === 'wrong'
|
|
550
|
+
? red(row.verdict)
|
|
551
|
+
: yellow(row.verdict)
|
|
552
|
+
: '';
|
|
553
|
+
const where = row.run ? dim(`run ${row.run}`) : '';
|
|
554
|
+
write(`${when} ${cyan(row.id)} rev ${row.caseRev} ${bold(row.kind)} ${[verdict, where].filter(Boolean).join(' ')} ${row.text} ${by}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const sessions = [
|
|
558
|
+
...new Set(rows.flatMap((r) => (r.by.session ? [r.by.session] : [])).reverse()),
|
|
559
|
+
];
|
|
560
|
+
const kept = sessions.filter(sessionKept).slice(0, 5);
|
|
561
|
+
if (kept.length > 0) {
|
|
562
|
+
note('');
|
|
563
|
+
for (const id of kept) {
|
|
564
|
+
note(dim(`resume: zen meta resume ${run.project} ${id}`));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
// drift
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
function driftCommand(run) {
|
|
572
|
+
const store = DatasetStore.open(run.root);
|
|
573
|
+
requireDataset(store);
|
|
574
|
+
const report = drift(store, { all: run.values.all });
|
|
575
|
+
if (run.ctx.json) {
|
|
576
|
+
return json(report);
|
|
577
|
+
}
|
|
578
|
+
write(`revision ${report.revision} · ${report.unchanged} unchanged · ${report.untracked} with no source`);
|
|
579
|
+
for (const f of report.files) {
|
|
580
|
+
const state = {
|
|
581
|
+
missing: red('missing'),
|
|
582
|
+
changed: yellow('changed'),
|
|
583
|
+
unsettled: dim('unsettled'),
|
|
584
|
+
}[f.state];
|
|
585
|
+
write(`${state} ${f.file}`);
|
|
586
|
+
}
|
|
587
|
+
for (const c of report.cases) {
|
|
588
|
+
const tag = c.state === 'gone' ? red('gone ') : yellow('changed');
|
|
589
|
+
write(` ${tag} ${cyan(c.id)} ${c.file} ${dim(anchorText(c.anchor))}${c.why ? dim(` — ${c.why}`) : ''}`);
|
|
590
|
+
}
|
|
591
|
+
for (const u of report.uncovered) {
|
|
592
|
+
write(` ${dim('uncovered')} ${u.file} ${anchorText(u.anchor)}`);
|
|
593
|
+
}
|
|
594
|
+
if (report.clean) {
|
|
595
|
+
note(green('clean: every case matches its source'));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
note('');
|
|
599
|
+
note(dim('re-read those sections, then: zen meta dataset apply <proposal> --partial --why "…"'));
|
|
600
|
+
if (report.cases.some((c) => c.state === 'gone')) {
|
|
601
|
+
note(dim('gone: retire the case, or reanchor it if the section was only renamed'));
|
|
602
|
+
}
|
|
603
|
+
if (report.uncovered.length > 0) {
|
|
604
|
+
note(dim('uncovered: new cases, or zen meta dataset ignore <file> --heading "…" --why "…"'));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
// The writes
|
|
609
|
+
// ---------------------------------------------------------------------------
|
|
610
|
+
function report(run, plan, dryRun) {
|
|
611
|
+
const r = plan.revision;
|
|
612
|
+
if (run.ctx.json) {
|
|
613
|
+
return json({
|
|
614
|
+
dryRun,
|
|
615
|
+
revision: plan.rows.length > 0 ? plan.rev : plan.manifest.revision,
|
|
616
|
+
added: r.added,
|
|
617
|
+
updated: r.updated,
|
|
618
|
+
retired: r.retired,
|
|
619
|
+
restored: r.restored,
|
|
620
|
+
restarted: r.restarted,
|
|
621
|
+
refreshed: plan.refreshed.map((c) => c.id),
|
|
622
|
+
unchanged: plan.unchanged.length,
|
|
623
|
+
...(dryRun
|
|
624
|
+
? {
|
|
625
|
+
changes: plan.rows.map((row) => ({
|
|
626
|
+
id: row.case.id,
|
|
627
|
+
op: row.op,
|
|
628
|
+
changes: row.changes,
|
|
629
|
+
})),
|
|
630
|
+
}
|
|
631
|
+
: {}),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
const say = (label, ids, paint) => {
|
|
635
|
+
if (ids.length > 0) {
|
|
636
|
+
write(`${paint(label)} ${ids.length} ${ids.join(', ')}`);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
say('added ', r.added, green);
|
|
640
|
+
say('updated ', r.updated, yellow);
|
|
641
|
+
say('retired ', r.retired, red);
|
|
642
|
+
say('restored ', r.restored, green);
|
|
643
|
+
say('restarted', r.restarted, bold);
|
|
644
|
+
say('refreshed', plan.refreshed.map((c) => c.id), dim);
|
|
645
|
+
if (dryRun) {
|
|
646
|
+
for (const row of plan.rows.filter((x) => x.op !== 'add')) {
|
|
647
|
+
write(dim(` ${row.case.id}: ${row.changes.map((c) => c.path).join(', ')}`));
|
|
648
|
+
}
|
|
649
|
+
note(dim('dry run: nothing written'));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
note(plan.rows.length > 0
|
|
653
|
+
? `revision ${plan.rev}${plan.unchanged.length ? dim(` · ${plan.unchanged.length} unchanged`) : ''}`
|
|
654
|
+
: 'nothing changed');
|
|
655
|
+
}
|
|
656
|
+
function apply(run) {
|
|
657
|
+
const [file, ...extra] = run.rest;
|
|
658
|
+
if (!file || extra.length > 0) {
|
|
659
|
+
throw usageError('apply takes one proposal file', 'zen meta dataset apply <file> --why "…"');
|
|
660
|
+
}
|
|
661
|
+
const at = resolve(run.ctx.cwd, file);
|
|
662
|
+
const proposed = readProposal(readJsonFile(at), at, run.root);
|
|
663
|
+
const dryRun = Boolean(run.values['dry-run']);
|
|
664
|
+
const why = dryRun ? (run.values.why ?? '') : needWhy(run.values);
|
|
665
|
+
const options = { partial: Boolean(run.values.partial), why, by: whoIsThis() };
|
|
666
|
+
if (dryRun) {
|
|
667
|
+
return report(run, planApply(DatasetStore.open(run.root), proposed, options), true);
|
|
668
|
+
}
|
|
669
|
+
writing(run.root, (store) => {
|
|
670
|
+
const plan = planApply(store, proposed, options);
|
|
671
|
+
store.commit(plan);
|
|
672
|
+
report(run, plan, false);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function keyValue(text, flag) {
|
|
676
|
+
const eq = text.indexOf('=');
|
|
677
|
+
if (eq <= 0) {
|
|
678
|
+
throw usageError(`${flag} takes key=value`, `got "${text}"`);
|
|
679
|
+
}
|
|
680
|
+
return [text.slice(0, eq).trim(), text.slice(eq + 1)];
|
|
681
|
+
}
|
|
682
|
+
const SETTABLE = ['class', 'complexity', 'expected', 'notes', 'input'];
|
|
683
|
+
function update(run) {
|
|
684
|
+
const [id, ...extra] = run.rest;
|
|
685
|
+
if (!id || extra.length > 0) {
|
|
686
|
+
throw usageError('update takes one case id', 'zen meta dataset update <id> --set class=x --why "…"');
|
|
687
|
+
}
|
|
688
|
+
const v = run.values;
|
|
689
|
+
const why = needWhy(v);
|
|
690
|
+
const touched = new Set();
|
|
691
|
+
const sets = (v.set ?? []).map((s) => keyValue(s, '--set'));
|
|
692
|
+
for (const [key, value] of sets) {
|
|
693
|
+
if (!SETTABLE.includes(key)) {
|
|
694
|
+
throw usageError(`--set ${key}: not a field --set can write`, `one of: ${SETTABLE.join(', ')}`);
|
|
695
|
+
}
|
|
696
|
+
if (key === 'complexity' && value && !COMPLEXITIES.includes(value)) {
|
|
697
|
+
throw usageError(`complexity must be one of ${COMPLEXITIES.join(', ')}`);
|
|
698
|
+
}
|
|
699
|
+
if (key === 'input' && !value.trim()) {
|
|
700
|
+
throw usageError('input cannot be empty');
|
|
701
|
+
}
|
|
702
|
+
touched.add(key);
|
|
703
|
+
}
|
|
704
|
+
const edits = (v['rubric-edit'] ?? []).map((s) => keyValue(s, '--rubric-edit'));
|
|
705
|
+
if (v['rubric-add'] || v['rubric-edit'] || v['rubric-drop']) {
|
|
706
|
+
touched.add('rubric');
|
|
707
|
+
}
|
|
708
|
+
if (v['tag-add'] || v['tag-drop']) {
|
|
709
|
+
touched.add('tags');
|
|
710
|
+
}
|
|
711
|
+
if (touched.size === 0) {
|
|
712
|
+
throw usageError('nothing to update', 'use --set, --rubric-add/-edit/-drop or --tag-add/-drop');
|
|
713
|
+
}
|
|
714
|
+
writing(run.root, (store) => {
|
|
715
|
+
const plan = planEdit(store, id, (c) => {
|
|
716
|
+
const fields = c;
|
|
717
|
+
for (const [key, value] of sets) {
|
|
718
|
+
if (value === '' && key !== 'input') {
|
|
719
|
+
delete fields[key];
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
fields[key] = value;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
for (const [rid, text] of edits) {
|
|
726
|
+
const item = c.rubric.find((r) => r.id === rid);
|
|
727
|
+
if (!item) {
|
|
728
|
+
throw usageError(`${id} has no rubric item ${rid}`, `it has: ${c.rubric.map((r) => r.id).join(', ')}`);
|
|
729
|
+
}
|
|
730
|
+
item.text = text;
|
|
731
|
+
}
|
|
732
|
+
for (const rid of v['rubric-drop'] ?? []) {
|
|
733
|
+
if (!c.rubric.some((r) => r.id === rid)) {
|
|
734
|
+
throw usageError(`${id} has no rubric item ${rid}`);
|
|
735
|
+
}
|
|
736
|
+
c.rubric = c.rubric.filter((r) => r.id !== rid);
|
|
737
|
+
}
|
|
738
|
+
let next = c.rubric.reduce((max, r) => Math.max(max, Number(/^r(\d+)$/.exec(r.id)?.[1] ?? 0)), 0);
|
|
739
|
+
for (const text of v['rubric-add'] ?? []) {
|
|
740
|
+
c.rubric.push({ id: `r${++next}`, text });
|
|
741
|
+
}
|
|
742
|
+
const tags = new Set(c.tags ?? []);
|
|
743
|
+
for (const t of v['tag-add'] ?? []) {
|
|
744
|
+
tags.add(t.trim());
|
|
745
|
+
}
|
|
746
|
+
for (const t of v['tag-drop'] ?? []) {
|
|
747
|
+
tags.delete(t.trim());
|
|
748
|
+
}
|
|
749
|
+
if (tags.size > 0) {
|
|
750
|
+
c.tags = [...tags].sort();
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
delete c.tags;
|
|
754
|
+
}
|
|
755
|
+
return c;
|
|
756
|
+
}, { why, by: whoIsThis(), override: v.override ? [...touched] : undefined });
|
|
757
|
+
store.commit(plan);
|
|
758
|
+
report(run, plan, false);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function setStatus(run, to) {
|
|
762
|
+
if (run.rest.length === 0) {
|
|
763
|
+
throw usageError(`${to === 'retired' ? 'retire' : 'restore'} which cases?`);
|
|
764
|
+
}
|
|
765
|
+
const why = needWhy(run.values);
|
|
766
|
+
writing(run.root, (store) => {
|
|
767
|
+
const plan = planStatus(store, run.rest, to, { why, by: whoIsThis() });
|
|
768
|
+
store.commit(plan);
|
|
769
|
+
report(run, plan, false);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function anchorFlags(v) {
|
|
773
|
+
if (v.heading !== undefined && v.pointer !== undefined) {
|
|
774
|
+
throw usageError('--heading or --pointer, not both');
|
|
775
|
+
}
|
|
776
|
+
const issues = [];
|
|
777
|
+
const anchor = readAnchor(v.heading !== undefined
|
|
778
|
+
? { heading: v.heading }
|
|
779
|
+
: v.pointer !== undefined
|
|
780
|
+
? { pointer: v.pointer }
|
|
781
|
+
: undefined, 'anchor', issues);
|
|
782
|
+
if (issues.length > 0) {
|
|
783
|
+
throw usageError(issues.join('; '));
|
|
784
|
+
}
|
|
785
|
+
return anchor;
|
|
786
|
+
}
|
|
787
|
+
function reanchor(run) {
|
|
788
|
+
const [id, ...extra] = run.rest;
|
|
789
|
+
if (!id || extra.length > 0) {
|
|
790
|
+
throw usageError('reanchor takes one case id', 'zen meta dataset reanchor <id> --heading "A > B" --why "…"');
|
|
791
|
+
}
|
|
792
|
+
const why = needWhy(run.values);
|
|
793
|
+
const anchor = anchorFlags(run.values);
|
|
794
|
+
writing(run.root, (store) => {
|
|
795
|
+
const plan = planEdit(store, id, (c) => {
|
|
796
|
+
const file = run.values.file ?? c.source?.file;
|
|
797
|
+
if (!file) {
|
|
798
|
+
throw usageError(`${id} has no source yet`, 'name one with --file');
|
|
799
|
+
}
|
|
800
|
+
c.source = { file, ...(anchor ? { anchor } : {}) };
|
|
801
|
+
return c;
|
|
802
|
+
}, { why, by: whoIsThis() });
|
|
803
|
+
store.commit(plan);
|
|
804
|
+
report(run, plan, false);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function ignore(run) {
|
|
808
|
+
const [file, ...extra] = run.rest;
|
|
809
|
+
if (!file || extra.length > 0) {
|
|
810
|
+
throw usageError('ignore takes one source file', 'zen meta dataset ignore <file> --heading "A > B" --why "…"');
|
|
811
|
+
}
|
|
812
|
+
const anchor = anchorFlags(run.values);
|
|
813
|
+
if (!anchor) {
|
|
814
|
+
throw usageError('ignore which section?', 'name it with --heading or --pointer');
|
|
815
|
+
}
|
|
816
|
+
const why = run.values.drop ? (run.values.why ?? '') : needWhy(run.values);
|
|
817
|
+
writing(run.root, (store) => {
|
|
818
|
+
requireDataset(store);
|
|
819
|
+
const others = store.manifest.ignored.filter((i) => !(i.file === file && sameAnchor(i.anchor, anchor)));
|
|
820
|
+
if (run.values.drop && others.length === store.manifest.ignored.length) {
|
|
821
|
+
throw usageError(`${file} ${anchorText(anchor)} is not ignored`);
|
|
822
|
+
}
|
|
823
|
+
const ignored = run.values.drop
|
|
824
|
+
? others
|
|
825
|
+
: [...others, { file, anchor, why, rev: store.manifest.revision }];
|
|
826
|
+
store.commit(planIgnored(store, ignored));
|
|
827
|
+
if (run.ctx.json) {
|
|
828
|
+
return json({ ignored: store.manifest.ignored });
|
|
829
|
+
}
|
|
830
|
+
note(`${run.values.drop ? 'no longer ignored' : 'ignored'}: ${file} ${anchorText(anchor)}`);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function addNote(run) {
|
|
834
|
+
const v = run.values;
|
|
835
|
+
if (run.rest.length === 0) {
|
|
836
|
+
throw usageError('note which cases?', 'zen meta dataset note <id>... -m "…"');
|
|
837
|
+
}
|
|
838
|
+
const text = v.message?.trim();
|
|
839
|
+
if (!text) {
|
|
840
|
+
throw usageError('a note needs -m <text>');
|
|
841
|
+
}
|
|
842
|
+
const verdicts = oneOf(v.verdict, VERDICTS, '--verdict');
|
|
843
|
+
if (verdicts && verdicts.length > 1) {
|
|
844
|
+
throw usageError('one --verdict per note');
|
|
845
|
+
}
|
|
846
|
+
const kind = (v.kind ?? (verdicts ? 'graded' : 'observation'));
|
|
847
|
+
oneOf([kind], NOTE_KINDS, '--kind');
|
|
848
|
+
const store = DatasetStore.open(run.root);
|
|
849
|
+
requireDataset(store);
|
|
850
|
+
const cases = run.rest.map((id) => {
|
|
851
|
+
const c = store.get(id);
|
|
852
|
+
if (!c) {
|
|
853
|
+
throw invalidError(`no case ${id}`);
|
|
854
|
+
}
|
|
855
|
+
return c;
|
|
856
|
+
});
|
|
857
|
+
let rubric;
|
|
858
|
+
if (v.rubric !== undefined) {
|
|
859
|
+
rubric = {};
|
|
860
|
+
for (const pair of v.rubric
|
|
861
|
+
.split(',')
|
|
862
|
+
.map((s) => s.trim())
|
|
863
|
+
.filter(Boolean)) {
|
|
864
|
+
const [rid, result] = keyValue(pair, '--rubric');
|
|
865
|
+
if (result !== 'pass' && result !== 'fail') {
|
|
866
|
+
throw usageError(`--rubric ${rid}: pass or fail`, `got "${result}"`);
|
|
867
|
+
}
|
|
868
|
+
for (const c of cases) {
|
|
869
|
+
if (!c.rubric.some((r) => r.id === rid)) {
|
|
870
|
+
throw usageError(`${c.id} has no rubric item ${rid}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
rubric[rid] = result;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const by = whoIsThis();
|
|
877
|
+
const at = new Date().toISOString();
|
|
878
|
+
for (const c of cases) {
|
|
879
|
+
store.note(c.id, {
|
|
880
|
+
type: 'note',
|
|
881
|
+
at,
|
|
882
|
+
kind,
|
|
883
|
+
caseRev: c.rev,
|
|
884
|
+
...(v.run ? { run: v.run } : {}),
|
|
885
|
+
...(verdicts ? { verdict: verdicts[0] } : {}),
|
|
886
|
+
...(rubric ? { rubric } : {}),
|
|
887
|
+
text,
|
|
888
|
+
by,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
if (run.ctx.json) {
|
|
892
|
+
return json({ noted: cases.map((c) => c.id), kind });
|
|
893
|
+
}
|
|
894
|
+
note(`noted ${cases.length} case(s) ${dim(kind)}`);
|
|
895
|
+
}
|
|
896
|
+
// ---------------------------------------------------------------------------
|
|
897
|
+
// sample / export
|
|
898
|
+
// ---------------------------------------------------------------------------
|
|
899
|
+
const FORMATS = ['ids', 'batch', 'cases', 'json'];
|
|
900
|
+
function drawOf(run, verb) {
|
|
901
|
+
const v = run.values;
|
|
902
|
+
const by = (v.by ?? (verb === 'sample' ? 'class' : 'none'))
|
|
903
|
+
.split(',')
|
|
904
|
+
.map((s) => s.trim())
|
|
905
|
+
.filter(Boolean);
|
|
906
|
+
const fields = by.length === 1 && by[0] === 'none'
|
|
907
|
+
? []
|
|
908
|
+
: (oneOf(by, STRATA_FIELDS, '--by') ?? []);
|
|
909
|
+
const order = oneOf((v.order ?? (verb === 'sample' ? 'rubric,complexity' : 'id'))
|
|
910
|
+
.split(',')
|
|
911
|
+
.map((s) => s.trim())
|
|
912
|
+
.filter(Boolean), ORDER_KEYS, '--order') ?? [];
|
|
913
|
+
const weights = new Map();
|
|
914
|
+
for (const w of v.weight ?? []) {
|
|
915
|
+
const at = w.lastIndexOf('=');
|
|
916
|
+
const weight = Number(w.slice(at + 1));
|
|
917
|
+
if (at <= 0 || !Number.isFinite(weight) || weight < 0) {
|
|
918
|
+
throw usageError('--weight takes <stratum>=<number>', `got "${w}"`);
|
|
919
|
+
}
|
|
920
|
+
weights.set(w.slice(0, at), weight);
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
by: fields,
|
|
924
|
+
weights,
|
|
925
|
+
order: verb === 'export' ? ['id'] : order,
|
|
926
|
+
count: whole(v.count, '-n', 1),
|
|
927
|
+
seed: whole(v.seed, '--seed') ?? 1,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function draw(run, verb) {
|
|
931
|
+
const v = run.values;
|
|
932
|
+
const store = DatasetStore.open(run.root);
|
|
933
|
+
requireDataset(store);
|
|
934
|
+
const at = whole(v.at, '--at', 1);
|
|
935
|
+
const cases = casesAt(store, at);
|
|
936
|
+
const notes = notesOf(store);
|
|
937
|
+
const criteria = criteriaOf(run, store, cases, at);
|
|
938
|
+
if (verb === 'export' && run.rest.length > 0) {
|
|
939
|
+
const known = new Set(cases.map((c) => c.id));
|
|
940
|
+
const missing = run.rest.filter((id) => !known.has(id));
|
|
941
|
+
if (missing.length > 0) {
|
|
942
|
+
throw invalidError(`no such case: ${missing.join(', ')}`);
|
|
943
|
+
}
|
|
944
|
+
const from = criteria.idsFrom;
|
|
945
|
+
criteria.idsFrom = new Set(run.rest.filter((id) => !from || from.has(id)));
|
|
946
|
+
}
|
|
947
|
+
else if (run.rest.length > 0) {
|
|
948
|
+
throw usageError('sample takes no ids', 'narrow it with --id <glob> or --ids-from <file>');
|
|
949
|
+
}
|
|
950
|
+
const plan = drawOf(run, verb);
|
|
951
|
+
const format = v.format ?? (run.ctx.json ? 'json' : verb === 'sample' ? 'ids' : 'cases');
|
|
952
|
+
if (!FORMATS.includes(format)) {
|
|
953
|
+
throw usageError(`--format takes ${FORMATS.join(', ')}`, `got "${format}"`);
|
|
954
|
+
}
|
|
955
|
+
const drawn = sample(cases, criteria, plan, notes);
|
|
956
|
+
const byId = new Map(cases.map((c) => [c.id, c]));
|
|
957
|
+
const chosen = drawn.ids.map((id) => byId.get(id));
|
|
958
|
+
const revision = at ?? store.manifest.revision;
|
|
959
|
+
const out = v.out ? resolve(run.ctx.cwd, v.out) : undefined;
|
|
960
|
+
let text;
|
|
961
|
+
switch (format) {
|
|
962
|
+
case 'ids':
|
|
963
|
+
text = drawn.ids.map((id) => `${id}\n`).join('');
|
|
964
|
+
break;
|
|
965
|
+
case 'batch':
|
|
966
|
+
text = `${JSON.stringify(batchInput(chosen, run.root, out), null, 2)}\n`;
|
|
967
|
+
break;
|
|
968
|
+
case 'cases':
|
|
969
|
+
text = `${JSON.stringify(dump(chosen, run.root, revision, out), null, 2)}\n`;
|
|
970
|
+
break;
|
|
971
|
+
default:
|
|
972
|
+
text = `${JSON.stringify({
|
|
973
|
+
datasetRev: revision,
|
|
974
|
+
criteria: Object.fromEntries(Object.entries(v).filter(([k]) => !['format', 'out', 'quiet', 'project'].includes(k))),
|
|
975
|
+
seed: plan.seed,
|
|
976
|
+
by: plan.by,
|
|
977
|
+
order: plan.order,
|
|
978
|
+
strata: drawn.strata,
|
|
979
|
+
matched: drawn.matched,
|
|
980
|
+
ids: drawn.ids,
|
|
981
|
+
}, null, 2)}\n`;
|
|
982
|
+
}
|
|
983
|
+
if (out) {
|
|
984
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
985
|
+
writeFileSync(out, text);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
process.stdout.write(text);
|
|
989
|
+
}
|
|
990
|
+
if (v.quiet) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (verb === 'sample' && drawn.strata.length > 0) {
|
|
994
|
+
note(table([
|
|
995
|
+
['STRATUM', 'CHOSEN', 'AVAILABLE', 'WEIGHT'].map((h) => bold(h)),
|
|
996
|
+
...drawn.strata.map((s) => [
|
|
997
|
+
s.key,
|
|
998
|
+
String(s.chosen),
|
|
999
|
+
String(s.available),
|
|
1000
|
+
String(s.weight),
|
|
1001
|
+
]),
|
|
1002
|
+
]).join('\n'));
|
|
1003
|
+
}
|
|
1004
|
+
if (drawn.ids.length === 0) {
|
|
1005
|
+
note(yellow(`no case matches${run.ctx.args.length > 1 ? `: ${run.ctx.args.slice(1).join(' ')}` : ''}`));
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
note(dim(`${drawn.ids.length} of ${drawn.matched} matching case(s), revision ${revision}${out ? ` → ${out}` : ''}`));
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
//# sourceMappingURL=command.js.map
|