@zenera/cli 1.1.24 → 1.1.26
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 +65 -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 +31 -1
- package/dist/catalog.js +140 -7
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +52 -2
- package/dist/commands/inspect.js +220 -31
- package/dist/commands/memory.js +156 -32
- package/dist/commands/meta.js +333 -24
- package/dist/commands/models.js +97 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +148 -16
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +8 -1
- package/dist/engine.js +7 -0
- package/dist/eventlog.d.ts +64 -0
- package/dist/eventlog.js +170 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +35 -3
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +43 -0
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -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 +516 -0
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -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 +127 -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 +57 -0
- package/dist/meta/finetune/feedback.js +104 -0
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +98 -0
- package/dist/meta/finetune/improvements.js +501 -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 +84 -0
- package/dist/meta/finetune/live.js +272 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +163 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +40 -0
- package/dist/meta/finetune/report.js +1397 -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 +47 -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 +43 -0
- package/dist/meta/finetune/train.js +425 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +316 -0
- package/dist/meta/finetune/tuning.d.ts +193 -0
- package/dist/meta/finetune/tuning.js +308 -0
- package/dist/meta/finetune/usage.d.ts +66 -0
- package/dist/meta/finetune/usage.js +260 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +15 -0
- package/dist/meta/host.js +21 -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/meta.d.ts +71 -1
- package/dist/meta.js +303 -6
- 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/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -7
- package/dist/scaffold.js +24 -67
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +107 -7
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +35 -0
- package/dist/tui/models.js +447 -0
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -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 +4 -2
- package/templates/editor/.github/copilot-instructions.md +60 -145
- package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -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 +5 -4
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -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 +137 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
- 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/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
- 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 +1492 -660
- 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 +74 -26
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- 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/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -494
- package/templates/project/agents/tools-instructions.md +14 -4
- package/templates/project/gitignore +2 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DatasetStore } from './store.ts';
|
|
2
|
+
import { type Anchor } from './types.ts';
|
|
3
|
+
export type CaseDrift = 'changed' | 'gone';
|
|
4
|
+
export interface DriftedCase {
|
|
5
|
+
id: string;
|
|
6
|
+
file: string;
|
|
7
|
+
anchor?: Anchor;
|
|
8
|
+
state: CaseDrift;
|
|
9
|
+
why?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DriftReport {
|
|
12
|
+
revision: number;
|
|
13
|
+
/** `unsettled`: never recorded, because a section in it is still uncovered */
|
|
14
|
+
files: {
|
|
15
|
+
file: string;
|
|
16
|
+
state: 'changed' | 'missing' | 'unsettled';
|
|
17
|
+
}[];
|
|
18
|
+
cases: DriftedCase[];
|
|
19
|
+
/** active cases checked and found where they were */
|
|
20
|
+
unchanged: number;
|
|
21
|
+
/** active cases with no source to drift from */
|
|
22
|
+
untracked: number;
|
|
23
|
+
uncovered: {
|
|
24
|
+
file: string;
|
|
25
|
+
anchor: Anchor;
|
|
26
|
+
}[];
|
|
27
|
+
clean: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** `all` re-reads unchanged files too, to list their uncovered sections. */
|
|
30
|
+
export declare function drift(store: DatasetStore, options?: {
|
|
31
|
+
all?: boolean;
|
|
32
|
+
}): DriftReport;
|
|
33
|
+
//# sourceMappingURL=drift.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Sources } from "./anchors.js";
|
|
2
|
+
import { sameAnchor } from "./types.js";
|
|
3
|
+
/** `all` re-reads unchanged files too, to list their uncovered sections. */
|
|
4
|
+
export function drift(store, options = {}) {
|
|
5
|
+
const sources = new Sources(store.root);
|
|
6
|
+
const active = store.all().filter((c) => c.status === 'active');
|
|
7
|
+
const byFile = new Map();
|
|
8
|
+
let untracked = 0;
|
|
9
|
+
for (const c of active) {
|
|
10
|
+
if (!c.source) {
|
|
11
|
+
untracked++;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
byFile.set(c.source.file, [...(byFile.get(c.source.file) ?? []), c]);
|
|
15
|
+
}
|
|
16
|
+
for (const ignored of store.manifest.ignored) {
|
|
17
|
+
if (!byFile.has(ignored.file)) {
|
|
18
|
+
byFile.set(ignored.file, []);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const report = {
|
|
22
|
+
revision: store.manifest.revision,
|
|
23
|
+
files: [],
|
|
24
|
+
cases: [],
|
|
25
|
+
unchanged: 0,
|
|
26
|
+
untracked,
|
|
27
|
+
uncovered: [],
|
|
28
|
+
clean: true,
|
|
29
|
+
};
|
|
30
|
+
for (const [file, cases] of [...byFile].sort(([a], [b]) => a.localeCompare(b))) {
|
|
31
|
+
const now = sources.hash(file);
|
|
32
|
+
const was = store.manifest.sources[file]?.sha256;
|
|
33
|
+
const moved = now !== was;
|
|
34
|
+
if (now === undefined) {
|
|
35
|
+
report.files.push({ file, state: 'missing' });
|
|
36
|
+
}
|
|
37
|
+
else if (moved) {
|
|
38
|
+
report.files.push({ file, state: was ? 'changed' : 'unsettled' });
|
|
39
|
+
}
|
|
40
|
+
if (!moved && !options.all) {
|
|
41
|
+
report.unchanged += cases.length;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const c of cases) {
|
|
45
|
+
const found = sources.section(file, c.source.anchor);
|
|
46
|
+
const anchor = c.source.anchor ? { anchor: c.source.anchor } : {};
|
|
47
|
+
if (!found.found) {
|
|
48
|
+
report.cases.push({ id: c.id, file, ...anchor, state: 'gone', why: found.why });
|
|
49
|
+
}
|
|
50
|
+
else if (found.sha256 !== c.source.sha256) {
|
|
51
|
+
report.cases.push({ id: c.id, file, ...anchor, state: 'changed' });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
report.unchanged++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (now === undefined) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const ignored = store.manifest.ignored.filter((i) => i.file === file).map((i) => i.anchor);
|
|
61
|
+
const covered = [...cases.map((c) => c.source.anchor), ...ignored];
|
|
62
|
+
for (const anchor of sources.uncovered(file, covered)) {
|
|
63
|
+
if (!ignored.some((i) => sameAnchor(i, anchor))) {
|
|
64
|
+
report.uncovered.push({ file, anchor });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
report.clean = report.cases.length === 0 && report.uncovered.length === 0;
|
|
69
|
+
return report;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=drift.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Case, type Input } from './types.ts';
|
|
2
|
+
export declare function batchInput(cases: readonly Case[], root: string, out?: string): {
|
|
3
|
+
batch: {
|
|
4
|
+
id: string;
|
|
5
|
+
input: Input;
|
|
6
|
+
}[];
|
|
7
|
+
};
|
|
8
|
+
export declare function dump(cases: readonly Case[], root: string, revision: number, out?: string): {
|
|
9
|
+
version: number;
|
|
10
|
+
revision: number;
|
|
11
|
+
cases: {
|
|
12
|
+
input: Input;
|
|
13
|
+
id: string;
|
|
14
|
+
rev: number;
|
|
15
|
+
status: import("./types.ts").Status;
|
|
16
|
+
class?: string;
|
|
17
|
+
complexity?: import("./types.ts").Complexity;
|
|
18
|
+
tags?: string[];
|
|
19
|
+
rubric: import("./types.ts").RubricItem[];
|
|
20
|
+
expected?: string;
|
|
21
|
+
notes?: string;
|
|
22
|
+
source?: import("./types.ts").Source;
|
|
23
|
+
overrides?: string[];
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=export.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { MEDIA_KINDS } from "./types.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Cases leaving the store
|
|
5
|
+
//
|
|
6
|
+
// Two shapes. A batch input is exactly what `zen run batch` accepts — an id and
|
|
7
|
+
// an input, never a rubric, because whatever reaches the agent under test must
|
|
8
|
+
// not include the answer key. A dump is the whole case, rubric and all, for a
|
|
9
|
+
// grader or for another machine, and it reads back in through `apply`.
|
|
10
|
+
//
|
|
11
|
+
// Media paths are stored relative to the project. On the way out they are made
|
|
12
|
+
// relative to the file being written, which is how both `zen run batch` and
|
|
13
|
+
// `apply` read them back; written to stdout, they are absolute.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const URL_LIKE = /^[a-z][a-z0-9+.-]*:/i;
|
|
16
|
+
function rebase(input, root, out) {
|
|
17
|
+
if (typeof input === 'string') {
|
|
18
|
+
return input;
|
|
19
|
+
}
|
|
20
|
+
return input.map((part) => {
|
|
21
|
+
if (typeof part === 'string' || 'text' in part) {
|
|
22
|
+
return part;
|
|
23
|
+
}
|
|
24
|
+
const kind = MEDIA_KINDS.find((k) => typeof part[k] === 'string');
|
|
25
|
+
const ref = kind ? part[kind] : undefined;
|
|
26
|
+
if (!kind || !ref || URL_LIKE.test(ref)) {
|
|
27
|
+
return part;
|
|
28
|
+
}
|
|
29
|
+
const absolute = isAbsolute(ref) ? ref : resolve(root, ref);
|
|
30
|
+
const written = out
|
|
31
|
+
? relative(dirname(resolve(out)), absolute)
|
|
32
|
+
.split(sep)
|
|
33
|
+
.join('/')
|
|
34
|
+
: absolute;
|
|
35
|
+
return {
|
|
36
|
+
...part,
|
|
37
|
+
[kind]: written.startsWith('.') || isAbsolute(written) ? written : `./${written}`,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function batchInput(cases, root, out) {
|
|
42
|
+
return { batch: cases.map((c) => ({ id: c.id, input: rebase(c.input, root, out) })) };
|
|
43
|
+
}
|
|
44
|
+
export function dump(cases, root, revision, out) {
|
|
45
|
+
return {
|
|
46
|
+
version: 1,
|
|
47
|
+
revision,
|
|
48
|
+
cases: cases.map((c) => ({ ...c, input: rebase(c.input, root, out) })),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Matcher } from '@zenera/neo';
|
|
2
|
+
import { type Case, type NoteKind, type NoteRow, type Verdict } from './types.ts';
|
|
3
|
+
export interface Criteria {
|
|
4
|
+
status: 'active' | 'retired' | 'all';
|
|
5
|
+
classes?: string[];
|
|
6
|
+
complexities?: string[];
|
|
7
|
+
tags?: string[];
|
|
8
|
+
rubric?: boolean;
|
|
9
|
+
expected?: boolean;
|
|
10
|
+
ids?: Matcher;
|
|
11
|
+
exclude?: Matcher;
|
|
12
|
+
idsFrom?: ReadonlySet<string>;
|
|
13
|
+
changedSince?: number;
|
|
14
|
+
restarted?: ReadonlySet<string>;
|
|
15
|
+
verdicts?: Verdict[];
|
|
16
|
+
neverGraded?: boolean;
|
|
17
|
+
noteKinds?: NoteKind[];
|
|
18
|
+
gradedIn?: string[];
|
|
19
|
+
source?: Matcher;
|
|
20
|
+
anchor?: Matcher;
|
|
21
|
+
grep?: Matcher;
|
|
22
|
+
}
|
|
23
|
+
export declare const STRATA_FIELDS: readonly ["class", "complexity", "tag", "source", "rubric", "verdict"];
|
|
24
|
+
export type StratumField = (typeof STRATA_FIELDS)[number];
|
|
25
|
+
export declare const ORDER_KEYS: readonly ["rubric", "complexity", "verdict", "rev", "id"];
|
|
26
|
+
export type OrderKey = (typeof ORDER_KEYS)[number];
|
|
27
|
+
export interface Draw {
|
|
28
|
+
by: StratumField[];
|
|
29
|
+
weights: ReadonlyMap<string, number>;
|
|
30
|
+
order: OrderKey[];
|
|
31
|
+
count?: number;
|
|
32
|
+
seed: number;
|
|
33
|
+
}
|
|
34
|
+
export interface Stratum {
|
|
35
|
+
key: string;
|
|
36
|
+
weight: number;
|
|
37
|
+
available: number;
|
|
38
|
+
chosen: number;
|
|
39
|
+
}
|
|
40
|
+
export interface Sampled {
|
|
41
|
+
ids: string[];
|
|
42
|
+
strata: Stratum[];
|
|
43
|
+
/** cases that passed the filters */
|
|
44
|
+
matched: number;
|
|
45
|
+
}
|
|
46
|
+
export type NotesOf = (c: Case) => readonly NoteRow[];
|
|
47
|
+
export declare function lastVerdict(c: Case, notes: NotesOf): Verdict | undefined;
|
|
48
|
+
export declare function matches(c: Case, k: Criteria, notes: NotesOf): boolean;
|
|
49
|
+
export declare function sample(cases: readonly Case[], criteria: Criteria, draw: Draw, notes: NotesOf): Sampled;
|
|
50
|
+
//# sourceMappingURL=sample.d.ts.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { usageError } from "../../host.js";
|
|
2
|
+
import { anchorText, COMPLEXITIES, } from "./types.js";
|
|
3
|
+
export const STRATA_FIELDS = ['class', 'complexity', 'tag', 'source', 'rubric', 'verdict'];
|
|
4
|
+
export const ORDER_KEYS = ['rubric', 'complexity', 'verdict', 'rev', 'id'];
|
|
5
|
+
/** Notes about this revision of the case, oldest first. */
|
|
6
|
+
const current = (c, notes) => notes(c).filter((n) => n.caseRev === c.rev);
|
|
7
|
+
export function lastVerdict(c, notes) {
|
|
8
|
+
return current(c, notes).findLast((n) => n.verdict)?.verdict;
|
|
9
|
+
}
|
|
10
|
+
const textOf = (c) => [
|
|
11
|
+
typeof c.input === 'string'
|
|
12
|
+
? c.input
|
|
13
|
+
: c.input
|
|
14
|
+
.map((p) => (typeof p === 'string' ? p : 'text' in p ? p.text : ''))
|
|
15
|
+
.join('\n'),
|
|
16
|
+
...c.rubric.map((r) => r.text),
|
|
17
|
+
].join('\n');
|
|
18
|
+
export function matches(c, k, notes) {
|
|
19
|
+
if (k.status !== 'all' && c.status !== k.status) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (k.classes && !k.classes.includes(c.class ?? 'unclassified')) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (k.complexities && !k.complexities.includes(c.complexity ?? 'unrated')) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (k.tags && !(c.tags ?? []).some((t) => k.tags.includes(t))) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (k.rubric !== undefined && k.rubric !== c.rubric.length > 0) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (k.expected !== undefined && k.expected !== (c.expected !== undefined)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (k.ids && !k.ids(c.id)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (k.exclude?.(c.id)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (k.idsFrom && !k.idsFrom.has(c.id)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (k.changedSince !== undefined && c.rev <= k.changedSince) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (k.restarted && !k.restarted.has(c.id)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (k.source && !(c.source && k.source(c.source.file))) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (k.anchor && !(c.source && k.anchor(anchorText(c.source.anchor)))) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (k.grep && !k.grep(textOf(c))) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (k.verdicts || k.neverGraded || k.noteKinds || k.gradedIn) {
|
|
62
|
+
const mine = current(c, notes);
|
|
63
|
+
const verdict = mine.findLast((n) => n.verdict)?.verdict;
|
|
64
|
+
if (k.verdicts && !(verdict && k.verdicts.includes(verdict))) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (k.neverGraded && mine.some((n) => n.kind === 'graded' || n.verdict)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (k.noteKinds && !mine.some((n) => k.noteKinds.includes(n.kind))) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (k.gradedIn && !mine.some((n) => n.run !== undefined && k.gradedIn.includes(n.run))) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
function stratumValue(c, field, notes) {
|
|
80
|
+
switch (field) {
|
|
81
|
+
case 'class':
|
|
82
|
+
return c.class ?? 'unclassified';
|
|
83
|
+
case 'complexity':
|
|
84
|
+
return c.complexity ?? 'unrated';
|
|
85
|
+
case 'tag':
|
|
86
|
+
return c.tags?.length ? [...c.tags].sort().join(',') : 'untagged';
|
|
87
|
+
case 'source':
|
|
88
|
+
return c.source?.file ?? 'no source';
|
|
89
|
+
case 'rubric':
|
|
90
|
+
return c.rubric.length > 0 ? 'rubric' : 'no rubric';
|
|
91
|
+
case 'verdict':
|
|
92
|
+
return lastVerdict(c, notes) ?? 'ungraded';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const COMPLEX_RANK = { complex: 0, medium: 1, simple: 2 };
|
|
96
|
+
const VERDICT_RANK = { wrong: 0, void: 1, right: 3 };
|
|
97
|
+
function rank(c, key, notes) {
|
|
98
|
+
switch (key) {
|
|
99
|
+
case 'rubric':
|
|
100
|
+
return c.rubric.length > 0 ? 0 : 1;
|
|
101
|
+
case 'complexity':
|
|
102
|
+
return COMPLEX_RANK[c.complexity ?? ''] ?? COMPLEXITIES.length;
|
|
103
|
+
case 'verdict':
|
|
104
|
+
return VERDICT_RANK[lastVerdict(c, notes) ?? ''] ?? 2;
|
|
105
|
+
case 'rev':
|
|
106
|
+
return -c.rev;
|
|
107
|
+
case 'id':
|
|
108
|
+
return c.id;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Lehmer / Park-Miller: exact in a double, so the same on every machine. */
|
|
112
|
+
function lehmer(seed) {
|
|
113
|
+
let s = seed % 2147483647;
|
|
114
|
+
if (s <= 0) {
|
|
115
|
+
s += 2147483646;
|
|
116
|
+
}
|
|
117
|
+
return () => {
|
|
118
|
+
s = (s * 48271) % 2147483647;
|
|
119
|
+
return s;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** FNV-1a, so each stratum shuffles on its own and a new case elsewhere moves nothing here. */
|
|
123
|
+
function hash(text) {
|
|
124
|
+
let h = 2166136261;
|
|
125
|
+
for (let i = 0; i < text.length; i++) {
|
|
126
|
+
h ^= text.charCodeAt(i);
|
|
127
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
128
|
+
}
|
|
129
|
+
return h;
|
|
130
|
+
}
|
|
131
|
+
function ordered(cases, key, draw, notes) {
|
|
132
|
+
const next = lehmer(draw.seed + hash(key));
|
|
133
|
+
const shuffled = [...cases].sort((a, b) => a.id.localeCompare(b.id));
|
|
134
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
135
|
+
const j = next() % (i + 1);
|
|
136
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
137
|
+
}
|
|
138
|
+
const position = new Map(shuffled.map((c, i) => [c.id, i]));
|
|
139
|
+
return shuffled.sort((a, b) => {
|
|
140
|
+
for (const k of draw.order) {
|
|
141
|
+
const x = rank(a, k, notes);
|
|
142
|
+
const y = rank(b, k, notes);
|
|
143
|
+
if (x !== y) {
|
|
144
|
+
return x < y ? -1 : 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return position.get(a.id) - position.get(b.id);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export function sample(cases, criteria, draw, notes) {
|
|
151
|
+
const eligible = cases.filter((c) => matches(c, criteria, notes));
|
|
152
|
+
const groups = new Map();
|
|
153
|
+
for (const c of eligible) {
|
|
154
|
+
const key = draw.by.length === 0
|
|
155
|
+
? 'all'
|
|
156
|
+
: draw.by.map((f) => stratumValue(c, f, notes)).join(' · ');
|
|
157
|
+
groups.set(key, [...(groups.get(key) ?? []), c]);
|
|
158
|
+
}
|
|
159
|
+
const unknown = [...draw.weights.keys()].filter((k) => !groups.has(k));
|
|
160
|
+
if (unknown.length > 0 && eligible.length > 0) {
|
|
161
|
+
throw usageError(`no stratum ${unknown.map((k) => `"${k}"`).join(', ')}`, `the strata are: ${[...groups.keys()].sort().join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
const lanes = [...groups.keys()].sort().map((key) => ({
|
|
164
|
+
key,
|
|
165
|
+
weight: draw.weights.get(key) ?? 1,
|
|
166
|
+
queue: ordered(groups.get(key), key, draw, notes),
|
|
167
|
+
taken: 0,
|
|
168
|
+
current: 0,
|
|
169
|
+
}));
|
|
170
|
+
const ids = [];
|
|
171
|
+
let live = lanes.filter((l) => l.weight > 0 && l.queue.length > 0);
|
|
172
|
+
const limit = draw.count ?? Infinity;
|
|
173
|
+
while (live.length > 0 && ids.length < limit) {
|
|
174
|
+
const total = live.reduce((sum, l) => sum + l.weight, 0);
|
|
175
|
+
let best = live[0];
|
|
176
|
+
for (const lane of live) {
|
|
177
|
+
lane.current += lane.weight;
|
|
178
|
+
if (lane.current > best.current) {
|
|
179
|
+
best = lane;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
best.current -= total;
|
|
183
|
+
ids.push(best.queue[best.taken++].id);
|
|
184
|
+
live = live.filter((l) => l.taken < l.queue.length);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
ids,
|
|
188
|
+
matched: eligible.length,
|
|
189
|
+
strata: lanes.map((l) => ({
|
|
190
|
+
key: l.key,
|
|
191
|
+
weight: l.weight,
|
|
192
|
+
available: l.queue.length,
|
|
193
|
+
chosen: l.taken,
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=sample.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Anchor, type Complexity, type Input } from './types.ts';
|
|
2
|
+
export interface ProposedRubric {
|
|
3
|
+
id?: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Proposed {
|
|
7
|
+
id: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
complexity?: Complexity;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
input: Input;
|
|
12
|
+
rubric: ProposedRubric[];
|
|
13
|
+
expected?: string;
|
|
14
|
+
notes?: string;
|
|
15
|
+
source?: {
|
|
16
|
+
file: string;
|
|
17
|
+
anchor?: Anchor;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `{ heading: "A > B" }`, `{ heading: ["A", "B"] }`, `{ pointer: "/x/0" }`, or
|
|
22
|
+
* nothing for the whole file.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readAnchor(raw: unknown, where: string, issues: string[]): Anchor | undefined;
|
|
25
|
+
/** Inside the project: relative to it. Outside: absolute. */
|
|
26
|
+
export declare function projectPath(root: string, absolute: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Every case in a proposal, or a thrown list of what is wrong with it. Media
|
|
29
|
+
* paths resolve against the proposal file, the way `zen run batch` resolves
|
|
30
|
+
* them against its input, and are kept relative to the project from then on.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readProposal(doc: unknown, file: string, root: string): Proposed[];
|
|
33
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { invalidError } from "../../host.js";
|
|
4
|
+
import { CASE_ID, COMPLEXITIES, MEDIA_KINDS, } from "./types.js";
|
|
5
|
+
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
6
|
+
/** A url, a data uri: anything with a scheme is not a path to rebase. */
|
|
7
|
+
const URL_LIKE = /^[a-z][a-z0-9+.-]*:/i;
|
|
8
|
+
/**
|
|
9
|
+
* `{ heading: "A > B" }`, `{ heading: ["A", "B"] }`, `{ pointer: "/x/0" }`, or
|
|
10
|
+
* nothing for the whole file.
|
|
11
|
+
*/
|
|
12
|
+
export function readAnchor(raw, where, issues) {
|
|
13
|
+
if (raw === undefined || raw === null) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (!isObject(raw)) {
|
|
17
|
+
issues.push(`${where}: anchor must be an object`);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
if (raw.heading !== undefined && raw.pointer !== undefined) {
|
|
21
|
+
issues.push(`${where}: anchor names both a heading and a pointer`);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
if (raw.heading !== undefined) {
|
|
25
|
+
const path = typeof raw.heading === 'string'
|
|
26
|
+
? raw.heading.split(' > ')
|
|
27
|
+
: Array.isArray(raw.heading) && raw.heading.every((h) => typeof h === 'string')
|
|
28
|
+
? raw.heading
|
|
29
|
+
: undefined;
|
|
30
|
+
const clean = path?.map((h) => h.trim()).filter(Boolean);
|
|
31
|
+
if (!clean || clean.length === 0) {
|
|
32
|
+
issues.push(`${where}: anchor.heading must be a heading or a list of them`);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return { heading: clean };
|
|
36
|
+
}
|
|
37
|
+
if (raw.pointer !== undefined) {
|
|
38
|
+
if (typeof raw.pointer !== 'string' ||
|
|
39
|
+
!(raw.pointer === '' || raw.pointer.startsWith('/'))) {
|
|
40
|
+
issues.push(`${where}: anchor.pointer must be a JSON pointer like /cases/0`);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return { pointer: raw.pointer };
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Inside the project: relative to it. Outside: absolute. */
|
|
48
|
+
export function projectPath(root, absolute) {
|
|
49
|
+
const rel = relative(root, absolute);
|
|
50
|
+
return rel.startsWith('..') || isAbsolute(rel) ? absolute : rel.split(sep).join('/');
|
|
51
|
+
}
|
|
52
|
+
function readInput(raw, where, base, root, issues) {
|
|
53
|
+
if (typeof raw === 'string') {
|
|
54
|
+
return raw.trim() ? raw : (issues.push(`${where}: input is empty`), undefined);
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
57
|
+
issues.push(`${where}: input must be a string or a non-empty list of parts`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const parts = [];
|
|
61
|
+
raw.forEach((part, i) => {
|
|
62
|
+
if (typeof part === 'string') {
|
|
63
|
+
parts.push(part);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (isObject(part) && typeof part.text === 'string') {
|
|
67
|
+
parts.push({ text: part.text });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const kind = MEDIA_KINDS.find((k) => isObject(part) && typeof part[k] === 'string');
|
|
71
|
+
if (!kind || !isObject(part)) {
|
|
72
|
+
issues.push(`${where}: input[${i}] is neither text nor ${MEDIA_KINDS.join('/')}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const ref = part[kind];
|
|
76
|
+
let stored = ref;
|
|
77
|
+
if (!URL_LIKE.test(ref)) {
|
|
78
|
+
const at = resolve(base, ref);
|
|
79
|
+
if (!existsSync(at)) {
|
|
80
|
+
issues.push(`${where}: input[${i}] ${kind} ${ref} does not exist`);
|
|
81
|
+
}
|
|
82
|
+
stored = projectPath(root, at);
|
|
83
|
+
}
|
|
84
|
+
parts.push({
|
|
85
|
+
[kind]: stored,
|
|
86
|
+
...(typeof part.mimeType === 'string' ? { mimeType: part.mimeType } : {}),
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
return parts;
|
|
90
|
+
}
|
|
91
|
+
function readOne(raw, index, base, root, issues) {
|
|
92
|
+
if (!isObject(raw)) {
|
|
93
|
+
issues.push(`case ${index}: not an object`);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const id = raw.id;
|
|
97
|
+
if (typeof id !== 'string' || !CASE_ID.test(id) || id === '.' || id === '..') {
|
|
98
|
+
issues.push(`case ${index}: id must be letters, digits, dot, dash or underscore`);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const where = id;
|
|
102
|
+
const input = readInput(raw.input, where, base, root, issues);
|
|
103
|
+
const rubric = [];
|
|
104
|
+
if (raw.rubric !== undefined) {
|
|
105
|
+
if (!Array.isArray(raw.rubric)) {
|
|
106
|
+
issues.push(`${where}: rubric must be a list`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
raw.rubric.forEach((item, i) => {
|
|
110
|
+
if (typeof item === 'string' && item.trim()) {
|
|
111
|
+
rubric.push({ text: item });
|
|
112
|
+
}
|
|
113
|
+
else if (isObject(item) && typeof item.text === 'string' && item.text.trim()) {
|
|
114
|
+
rubric.push({
|
|
115
|
+
text: item.text,
|
|
116
|
+
...(typeof item.id === 'string' ? { id: item.id } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
issues.push(`${where}: rubric[${i}] is not a line of text`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let complexity;
|
|
126
|
+
if (raw.complexity !== undefined) {
|
|
127
|
+
if (COMPLEXITIES.includes(raw.complexity)) {
|
|
128
|
+
complexity = raw.complexity;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
issues.push(`${where}: complexity must be one of ${COMPLEXITIES.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const text = (key) => {
|
|
135
|
+
const v = raw[key];
|
|
136
|
+
if (v === undefined || v === null) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
if (typeof v !== 'string') {
|
|
140
|
+
issues.push(`${where}: ${key} must be a string`);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
return v;
|
|
144
|
+
};
|
|
145
|
+
let tags;
|
|
146
|
+
if (raw.tags !== undefined) {
|
|
147
|
+
if (Array.isArray(raw.tags) && raw.tags.every((t) => typeof t === 'string' && t.trim())) {
|
|
148
|
+
tags = [...new Set(raw.tags.map((t) => t.trim()))];
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
issues.push(`${where}: tags must be a list of words`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let source;
|
|
155
|
+
const rawSource = typeof raw.source === 'string' ? { file: raw.source } : raw.source;
|
|
156
|
+
if (rawSource !== undefined) {
|
|
157
|
+
if (!isObject(rawSource) || typeof rawSource.file !== 'string' || !rawSource.file) {
|
|
158
|
+
issues.push(`${where}: source must name a file`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const at = resolve(root, rawSource.file);
|
|
162
|
+
if (!existsSync(at)) {
|
|
163
|
+
issues.push(`${where}: source ${rawSource.file} does not exist`);
|
|
164
|
+
}
|
|
165
|
+
source = {
|
|
166
|
+
file: projectPath(root, at),
|
|
167
|
+
...(rawSource.anchor !== undefined
|
|
168
|
+
? { anchor: readAnchor(rawSource.anchor, where, issues) }
|
|
169
|
+
: {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!input) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const cls = text('class');
|
|
177
|
+
const expected = text('expected');
|
|
178
|
+
const notes = text('notes');
|
|
179
|
+
return {
|
|
180
|
+
id,
|
|
181
|
+
...(cls ? { class: cls } : {}),
|
|
182
|
+
...(complexity ? { complexity } : {}),
|
|
183
|
+
...(tags && tags.length > 0 ? { tags } : {}),
|
|
184
|
+
input,
|
|
185
|
+
rubric,
|
|
186
|
+
...(expected ? { expected } : {}),
|
|
187
|
+
...(notes ? { notes } : {}),
|
|
188
|
+
...(source ? { source } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Every case in a proposal, or a thrown list of what is wrong with it. Media
|
|
193
|
+
* paths resolve against the proposal file, the way `zen run batch` resolves
|
|
194
|
+
* them against its input, and are kept relative to the project from then on.
|
|
195
|
+
*/
|
|
196
|
+
export function readProposal(doc, file, root) {
|
|
197
|
+
const list = Array.isArray(doc) ? doc : isObject(doc) ? (doc.cases ?? doc.samples) : undefined;
|
|
198
|
+
if (!Array.isArray(list)) {
|
|
199
|
+
throw invalidError(`${file} holds no cases`, 'write { "cases": [ … ] } — or a bare list of cases');
|
|
200
|
+
}
|
|
201
|
+
const issues = [];
|
|
202
|
+
const base = dirname(resolve(file));
|
|
203
|
+
const cases = list.flatMap((raw, i) => readOne(raw, i, base, root, issues) ?? []);
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
for (const c of cases) {
|
|
206
|
+
if (seen.has(c.id)) {
|
|
207
|
+
issues.push(`${c.id}: the id appears more than once`);
|
|
208
|
+
}
|
|
209
|
+
seen.add(c.id);
|
|
210
|
+
}
|
|
211
|
+
if (issues.length > 0) {
|
|
212
|
+
throw invalidError(`${file}: ${issues.length} problem${issues.length === 1 ? '' : 's'}\n ${issues.join('\n ')}`, 'fix the proposal and apply it again; nothing was written');
|
|
213
|
+
}
|
|
214
|
+
return cases;
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=schema.js.map
|