orizu 0.5.21 → 0.5.22
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 +9 -6
- package/dist/gepa-engine-dispatch.d.ts.map +1 -1
- package/dist/gepa-engine-dispatch.js +20 -1
- package/dist/gepa-python-paths.d.ts.map +1 -1
- package/dist/gepa-python-paths.js +25 -6
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +36 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -39
- package/dist/instruction-set-loader.d.ts.map +1 -1
- package/dist/instruction-set-loader.js +57 -5
- package/dist/instruction-sets-cli.d.ts +5 -2
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +134 -24
- package/dist/skilled-proposer-config.d.ts +2 -0
- package/dist/skilled-proposer-config.d.ts.map +1 -0
- package/dist/skilled-proposer-config.js +212 -0
- package/dist/skilled-proposer-launch.d.ts +4 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -1
- package/dist/skilled-proposer-launch.js +65 -1
- package/package.json +1 -1
- package/src/gepa-engine-dispatch.ts +21 -1
- package/src/gepa-python-paths.ts +24 -6
- package/src/help.ts +36 -14
- package/src/index.ts +6 -53
- package/src/instruction-set-loader.ts +55 -5
- package/src/instruction-sets-cli.ts +173 -19
- package/src/skilled-proposer-config.ts +191 -0
- package/src/skilled-proposer-launch.ts +59 -1
- package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +36 -3
- package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +31 -3
- package/vendor/orizu-gepa/src/orizu_gepa_connector/skilled_proposer_bridge.py +354 -5
- package/vendor/skills/orizu-cli/SKILL.md +50 -16
- package/vendor/skills/orizu-cli/references/building-judges.md +2 -2
- package/vendor/skills/orizu-cli/references/cli-reference.md +57 -28
- package/vendor/skills/orizu-cli/references/instructions-after-prompts.md +120 -0
- package/vendor/skills/orizu-cli/references/migrate-existing-gepa-setup.md +109 -49
- package/vendor/skills/orizu-cli/references/optimization-reports.md +16 -11
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +29 -28
- package/vendor/skills/orizu-cli/references/primer.md +21 -5
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +132 -85
package/src/help.ts
CHANGED
|
@@ -89,6 +89,40 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
89
89
|
},
|
|
90
90
|
{ path: ['instructions', 'archive'], aliases: [['instruction-sets', 'archive']], usage: 'orizu instructions archive <slug-or-exact-name> --project <team/project> [--json]', summary: 'Archive an instruction set without changing resolution or sync.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions archive planner --project core/evals'] },
|
|
91
91
|
{ path: ['instructions', 'restore'], aliases: [['instruction-sets', 'restore']], usage: 'orizu instructions restore <slug-or-exact-name> --project <team/project> [--json]', summary: 'Restore an archived instruction set.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions restore planner --project core/evals'] },
|
|
92
|
+
{
|
|
93
|
+
path: ['instructions', 'scorers', 'set-headline'],
|
|
94
|
+
aliases: [['instruction-sets', 'scorers', 'set-headline']],
|
|
95
|
+
usage: 'orizu instructions scorers set-headline <set> --key <component-key> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]',
|
|
96
|
+
summary: 'Bind the headline scorer for one instruction-set component.',
|
|
97
|
+
group: 'Instruction sets',
|
|
98
|
+
options: [
|
|
99
|
+
{ name: '--key <component-key>', help: 'Instruction-set component key.', required: true },
|
|
100
|
+
{ name: '--scorer-version <id>', help: 'Scorer version to bind.', required: true },
|
|
101
|
+
{ name: '--dataset-version <id>', help: 'Default dataset version.' },
|
|
102
|
+
{ name: '--split-set <id>', help: 'Default dataset split set.' },
|
|
103
|
+
{ name: '--split <name>', help: 'Default split name.' },
|
|
104
|
+
{ name: '--project <team/project>', help: 'Project slug.' },
|
|
105
|
+
{ name: '--json', help: 'Print JSON.' },
|
|
106
|
+
],
|
|
107
|
+
examples: ['orizu instructions scorers set-headline planner --key system --scorer-version <id> --project core/evals'],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: ['instructions', 'scorers', 'add'],
|
|
111
|
+
aliases: [['instruction-sets', 'scorers', 'add']],
|
|
112
|
+
usage: 'orizu instructions scorers add <set> --key <component-key> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]',
|
|
113
|
+
summary: 'Track an additional scorer for one instruction-set component.',
|
|
114
|
+
group: 'Instruction sets',
|
|
115
|
+
options: [
|
|
116
|
+
{ name: '--key <component-key>', help: 'Instruction-set component key.', required: true },
|
|
117
|
+
{ name: '--scorer-version <id>', help: 'Scorer version to bind.', required: true },
|
|
118
|
+
{ name: '--dataset-version <id>', help: 'Default dataset version.' },
|
|
119
|
+
{ name: '--split-set <id>', help: 'Default dataset split set.' },
|
|
120
|
+
{ name: '--split <name>', help: 'Default split name.' },
|
|
121
|
+
{ name: '--project <team/project>', help: 'Project slug.' },
|
|
122
|
+
{ name: '--json', help: 'Print JSON.' },
|
|
123
|
+
],
|
|
124
|
+
examples: ['orizu instructions scorers add planner --key system --scorer-version <id> --project core/evals'],
|
|
125
|
+
},
|
|
92
126
|
{
|
|
93
127
|
path: ['login'],
|
|
94
128
|
usage: 'orizu login [--no-prompt-if-logged-in]',
|
|
@@ -413,18 +447,6 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
413
447
|
summary: 'Move or set a non-production prompt label; use instructions profiles promote for production.',
|
|
414
448
|
group: 'Prompts and judges',
|
|
415
449
|
},
|
|
416
|
-
{
|
|
417
|
-
path: ['prompts', 'scorers', 'set-headline'],
|
|
418
|
-
usage: 'orizu prompts scorers set-headline <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]',
|
|
419
|
-
summary: 'Bind the headline scorer for a prompt.',
|
|
420
|
-
group: 'Scorers and runners',
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
path: ['prompts', 'scorers', 'add'],
|
|
424
|
-
usage: 'orizu prompts scorers add <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]',
|
|
425
|
-
summary: 'Track an additional scorer for a prompt.',
|
|
426
|
-
group: 'Scorers and runners',
|
|
427
|
-
},
|
|
428
450
|
{
|
|
429
451
|
path: ['judges', 'list'],
|
|
430
452
|
usage: 'orizu judges list --project <team/project> [--status active|archived|all]',
|
|
@@ -578,8 +600,8 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
578
600
|
},
|
|
579
601
|
{
|
|
580
602
|
path: ['optimizations', 'run-gepa'],
|
|
581
|
-
usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> (--candidate-version-id <id> | --instruction-set <name> --model-config <identity> [--component-selector round-robin|all]) --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> [--engine official|legacy] [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n> | --max-candidate-proposals <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
|
|
582
|
-
summary: 'Run official GEPA locally and stream events; pass --engine legacy only for the frozen safety hatch. --instruction-set requires --model-config and cannot be combined with --candidate-version-id; round-robin updates one component-map entry per round and all updates every component. --max-candidate-proposals is official-engine-only. Validates the scorer contract on the seed before iterating; judge runners built for flat-row score runs need --scorer-input-contract flat_row (ALI-1158).',
|
|
603
|
+
usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> (--candidate-version-id <id> | --instruction-set <name> --model-config <identity> [--component-selector round-robin|all]) --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> [--engine official|legacy] [--candidate-proposer skilled-proposer [--candidate-proposer-config <@file|path>] [--proposal-max-calls <n>] [--proposal-max-tokens <n>]] [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n> | --max-candidate-proposals <n>] [--num-threads auto|N] [--reflection-max-tokens N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
|
|
604
|
+
summary: 'Run official GEPA locally and stream events; pass --engine legacy only for the frozen safety hatch. Skilled-proposer is official-only, its config requires that selection, and the selection cannot be combined with --reflection-prompt-template. --instruction-set requires --model-config and cannot be combined with --candidate-version-id; round-robin updates one component-map entry per round and all updates every component. --max-candidate-proposals is official-engine-only. Validates the scorer contract on the seed before iterating; judge runners built for flat-row score runs need --scorer-input-contract flat_row (ALI-1158).',
|
|
583
605
|
group: 'Optimizations',
|
|
584
606
|
},
|
|
585
607
|
{
|
package/src/index.ts
CHANGED
|
@@ -117,7 +117,7 @@ import { dispatchGepaEngine } from './gepa-engine-dispatch.js'
|
|
|
117
117
|
import { resolveGepaInstructionSetProfileVersion } from './instruction-set-gepa-launch.js'
|
|
118
118
|
import { getGepaPythonCommand } from './gepa-python-command.js'
|
|
119
119
|
import { getGepaPythonPathEntries } from './gepa-python-paths.js'
|
|
120
|
-
import { prepareSkilledProposerLaunch } from './skilled-proposer-launch.js'
|
|
120
|
+
import { prepareSkilledProposerLaunch, spawnSkilledProposerChild } from './skilled-proposer-launch.js'
|
|
121
121
|
|
|
122
122
|
export { parseJsonResponse, sanitizeTerminalText } from './json-response.js'
|
|
123
123
|
|
|
@@ -1767,53 +1767,6 @@ function refusePromptMutation(replacement: string): never {
|
|
|
1767
1767
|
throw new Error(`Use: orizu instructions ${replacement}`)
|
|
1768
1768
|
}
|
|
1769
1769
|
|
|
1770
|
-
async function bindPromptScorer(role: 'headline' | 'tracked') {
|
|
1771
|
-
const promptId = getPositionalArg(3)
|
|
1772
|
-
const project = getArg('--project') || await resolveProjectSlug(null)
|
|
1773
|
-
const scorerVersionId = getArg('--scorer-version')
|
|
1774
|
-
|
|
1775
|
-
if (!promptId || !scorerVersionId) {
|
|
1776
|
-
throw new Error(`Usage: orizu prompts scorers ${role === 'headline' ? 'set-headline' : 'add'} <prompt-id> --scorer-version <id> [--dataset-version <id> --split-set <id> --split <name>] [--project <team/project>] [--json]`)
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
const response = await authedFetch(`/api/cli/prompts/scorers?project=${encodeURIComponent(project)}`, {
|
|
1780
|
-
method: 'POST',
|
|
1781
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1782
|
-
body: JSON.stringify({
|
|
1783
|
-
promptId,
|
|
1784
|
-
scorerVersionId,
|
|
1785
|
-
role,
|
|
1786
|
-
datasetVersionId: getArg('--dataset-version') || undefined,
|
|
1787
|
-
splitSetId: getArg('--split-set') || undefined,
|
|
1788
|
-
splitName: getArg('--split') || undefined,
|
|
1789
|
-
}),
|
|
1790
|
-
})
|
|
1791
|
-
|
|
1792
|
-
if (!response.ok) {
|
|
1793
|
-
throw new Error(`Failed to bind prompt scorer: ${await response.text()}`)
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
const data = await parseJsonResponse<{
|
|
1797
|
-
promptScorer: Record<string, unknown>
|
|
1798
|
-
owner?: {
|
|
1799
|
-
instructionSetSlug: string
|
|
1800
|
-
componentKey: string
|
|
1801
|
-
}
|
|
1802
|
-
}>(response, 'Prompt scorer bind')
|
|
1803
|
-
if (hasJsonFlag()) {
|
|
1804
|
-
printJson(data)
|
|
1805
|
-
return
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
printLine(`${role === 'headline' ? 'Set headline' : 'Added'} scorer ${sanitizeTerminalText(scorerVersionId)} for ${sanitizeTerminalText(promptId)}`)
|
|
1809
|
-
if (data.owner) {
|
|
1810
|
-
printLine(
|
|
1811
|
-
`Owner: ${sanitizeTerminalText(promptId)} -> ` +
|
|
1812
|
-
`${sanitizeTerminalText(data.owner.instructionSetSlug)} / ${sanitizeTerminalText(data.owner.componentKey)}`
|
|
1813
|
-
)
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
1770
|
function getPositionalArg(index: number): string | null {
|
|
1818
1771
|
const value = cliArgs[index]
|
|
1819
1772
|
return value && !value.startsWith('--') ? value : null
|
|
@@ -2539,7 +2492,9 @@ async function runGepaOptimization() {
|
|
|
2539
2492
|
if (instructionSetProfileVersionId) dispatch.environment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_ID = instructionSetProfileVersionId
|
|
2540
2493
|
selectedEngine = dispatch.engine
|
|
2541
2494
|
const launch = prepareSkilledProposerLaunch(python, dispatch.engine, dispatch.environment)
|
|
2542
|
-
result =
|
|
2495
|
+
result = await spawnSkilledProposerChild(
|
|
2496
|
+
launch.python, ['-m', dispatch.module, ...dispatch.args], dispatch.engine, launch.environment,
|
|
2497
|
+
)
|
|
2543
2498
|
} finally {
|
|
2544
2499
|
verified.cleanup()
|
|
2545
2500
|
}
|
|
@@ -6236,13 +6191,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
6236
6191
|
}
|
|
6237
6192
|
|
|
6238
6193
|
if (command === 'prompts' && subcommand === 'scorers' && cliArgs[2] === 'set-headline') {
|
|
6239
|
-
|
|
6240
|
-
return
|
|
6194
|
+
refusePromptMutation('scorers set-headline <set> --key <component-key> --scorer-version <id> --project <team/project>')
|
|
6241
6195
|
}
|
|
6242
6196
|
|
|
6243
6197
|
if (command === 'prompts' && subcommand === 'scorers' && cliArgs[2] === 'add') {
|
|
6244
|
-
|
|
6245
|
-
return
|
|
6198
|
+
refusePromptMutation('scorers add <set> --key <component-key> --scorer-version <id> --project <team/project>')
|
|
6246
6199
|
}
|
|
6247
6200
|
|
|
6248
6201
|
if (command === 'judges' && subcommand === 'list') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, realpathSync, readFileSync, statSync } from 'node:fs'
|
|
1
|
+
import { existsSync, realpathSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
2
2
|
import { resolve, sep } from 'node:path'
|
|
3
3
|
|
|
4
4
|
export class InstructionSetLoaderError extends Error {
|
|
@@ -6,14 +6,64 @@ export class InstructionSetLoaderError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
interface Material { files: Record<string, string>; pinnedComponents?: Record<string, unknown> }
|
|
9
|
-
interface Manifest { manifestVersion: number; name: string; shape: string[]; default: Material; profiles: Array<{ modelConfigIdentity: string; production: Material | null }>; filteredTo?: string[] }
|
|
9
|
+
interface Manifest { manifestVersion: number; projectId?: string; instructionSetId?: string; name: string; slug?: string; shape: string[]; default: Material; profiles: Array<{ modelConfigIdentity: string; production: Material | null }>; filteredTo?: string[] }
|
|
10
10
|
|
|
11
11
|
function inside(root: string, candidate: string) { return candidate === root || candidate.startsWith(`${root}${sep}`) }
|
|
12
12
|
|
|
13
|
+
function manifestMatchesReference(candidate: string, reference: string): boolean {
|
|
14
|
+
try {
|
|
15
|
+
const manifest = JSON.parse(
|
|
16
|
+
readFileSync(resolve(candidate, 'manifest.json'), 'utf8')
|
|
17
|
+
) as Pick<Manifest, 'name' | 'slug'>
|
|
18
|
+
return manifest.name === reference || manifest.slug === reference
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveSetRoot(directory: string, reference: string): string {
|
|
25
|
+
let directoryRoot: string
|
|
26
|
+
try { directoryRoot = realpathSync(directory) } catch { throw new InstructionSetLoaderError('instruction_set_not_synced') }
|
|
27
|
+
const direct = resolve(directoryRoot, reference)
|
|
28
|
+
try {
|
|
29
|
+
if (inside(directoryRoot, direct) && existsSync(direct) && statSync(direct).isDirectory()) {
|
|
30
|
+
const realDirect = realpathSync(direct)
|
|
31
|
+
if (inside(directoryRoot, realDirect)) {
|
|
32
|
+
try {
|
|
33
|
+
const manifest = JSON.parse(
|
|
34
|
+
readFileSync(resolve(realDirect, 'manifest.json'), 'utf8')
|
|
35
|
+
) as Pick<Manifest, 'name' | 'slug'>
|
|
36
|
+
if (manifest.name === reference || manifest.slug === reference) {
|
|
37
|
+
return realDirect
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Preserve the exact-path loader's specific missing/invalid manifest error.
|
|
41
|
+
return realDirect
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// A stale or unreadable direct-path candidate must not shadow a valid set.
|
|
47
|
+
}
|
|
48
|
+
const matches = readdirSync(directoryRoot, { withFileTypes: true }).flatMap(entry => {
|
|
49
|
+
try {
|
|
50
|
+
if (entry.name.startsWith('.') || !entry.isDirectory()) return []
|
|
51
|
+
const candidate = realpathSync(resolve(directoryRoot, entry.name))
|
|
52
|
+
return inside(directoryRoot, candidate) && manifestMatchesReference(candidate, reference)
|
|
53
|
+
? [candidate]
|
|
54
|
+
: []
|
|
55
|
+
} catch {
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
if (matches.length !== 1) throw new InstructionSetLoaderError(
|
|
60
|
+
matches.length > 1 ? 'instruction_set_reference_ambiguous' : 'instruction_set_not_synced'
|
|
61
|
+
)
|
|
62
|
+
return matches[0]!
|
|
63
|
+
}
|
|
64
|
+
|
|
13
65
|
export function loadInstructionSet(directory: string, name: string, modelConfigIdentity: string): Record<string, string> {
|
|
14
|
-
const
|
|
15
|
-
if (!existsSync(unresolvedRoot) || !statSync(unresolvedRoot).isDirectory()) throw new InstructionSetLoaderError('instruction_set_not_synced')
|
|
16
|
-
const setRoot = realpathSync(unresolvedRoot)
|
|
66
|
+
const setRoot = resolveSetRoot(directory, name)
|
|
17
67
|
const manifestPath = resolve(setRoot, 'manifest.json')
|
|
18
68
|
if (!existsSync(manifestPath)) throw new InstructionSetLoaderError('instruction_set_manifest_missing')
|
|
19
69
|
let manifest: Manifest
|
|
@@ -43,13 +43,30 @@ const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/u
|
|
|
43
43
|
export interface SyncComponent { body?: string; repoPath?: string; contentSha?: string; commitSha?: string }
|
|
44
44
|
export interface SyncMaterial { profileVersionId: string; versionNumber: number; modelConfigIdentity: string; resolvedFrom: string; components: Record<string, SyncComponent> }
|
|
45
45
|
export interface SyncProfile { modelConfigIdentity: string; resolvedFrom: string; production: SyncMaterial | null }
|
|
46
|
-
export interface SyncSet {
|
|
46
|
+
export interface SyncSet {
|
|
47
|
+
projectId?: string
|
|
48
|
+
instructionSetId?: string
|
|
49
|
+
name: string
|
|
50
|
+
slug?: string
|
|
51
|
+
description?: string | null
|
|
52
|
+
shape: string[]
|
|
53
|
+
default: SyncMaterial
|
|
54
|
+
profiles: SyncProfile[]
|
|
55
|
+
filteredTo?: string[]
|
|
56
|
+
}
|
|
47
57
|
function safeSegment(value: string) { if (!SAFE_SEGMENT.test(value) || value === '.' || value === '..' || value.startsWith('.')) throw new Error('instruction_set_path_unsafe') }
|
|
58
|
+
function isSafeSegment(value: string) {
|
|
59
|
+
try { safeSegment(value); return true } catch { return false }
|
|
60
|
+
}
|
|
48
61
|
function slug(identity: string) { return identity.replaceAll('/', '__').replace(/[^A-Za-z0-9._-]/gu, '_') }
|
|
49
62
|
function stable(value: unknown): string { return `${JSON.stringify(value, null, 2)}\n` }
|
|
50
63
|
function sha256(value: string): string { return createHash('sha256').update(value, 'utf8').digest('hex') }
|
|
51
64
|
|
|
52
|
-
export interface SyncFileOps {
|
|
65
|
+
export interface SyncFileOps {
|
|
66
|
+
writeFileSync: typeof writeFileSync
|
|
67
|
+
renameSync?: typeof renameSync
|
|
68
|
+
rmSync?: typeof rmSync
|
|
69
|
+
}
|
|
53
70
|
|
|
54
71
|
function writeMaterial(root: string, material: SyncMaterial, shape: string[], manifestRoot: string, fileOps: SyncFileOps = { writeFileSync }) {
|
|
55
72
|
const files: Record<string, string> = {}
|
|
@@ -77,16 +94,76 @@ function removeOwnScratch(paths: Array<string | null>) {
|
|
|
77
94
|
for (const path of paths) if (path && existsSync(path)) rmSync(path, { recursive: true, force: true })
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
function existingDestinationIsSynced(
|
|
97
|
+
function existingDestinationIsSynced(
|
|
98
|
+
destination: string,
|
|
99
|
+
set: Pick<SyncSet, 'projectId' | 'instructionSetId' | 'name' | 'slug'>
|
|
100
|
+
) {
|
|
101
|
+
if (
|
|
102
|
+
set.slug &&
|
|
103
|
+
set.projectId === undefined &&
|
|
104
|
+
set.instructionSetId === undefined
|
|
105
|
+
) {
|
|
106
|
+
return existingDestinationIsLegacySynced(destination, set.name, set.slug)
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (!statSync(destination).isDirectory()) return false
|
|
110
|
+
const manifest = JSON.parse(readFileSync(join(destination, 'manifest.json'), 'utf8')) as {
|
|
111
|
+
manifestVersion?: number
|
|
112
|
+
projectId?: string
|
|
113
|
+
instructionSetId?: string
|
|
114
|
+
name?: string
|
|
115
|
+
slug?: string
|
|
116
|
+
}
|
|
117
|
+
return manifest.manifestVersion === 1 && (set.slug
|
|
118
|
+
? manifest.slug === set.slug &&
|
|
119
|
+
manifest.projectId === set.projectId &&
|
|
120
|
+
manifest.instructionSetId === set.instructionSetId
|
|
121
|
+
: manifest.name === set.name)
|
|
122
|
+
} catch { return false }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function existingDestinationIsLegacySynced(
|
|
126
|
+
destination: string,
|
|
127
|
+
name: string,
|
|
128
|
+
slug?: string
|
|
129
|
+
) {
|
|
81
130
|
try {
|
|
82
131
|
if (!statSync(destination).isDirectory()) return false
|
|
83
|
-
const manifest = JSON.parse(readFileSync(join(destination, 'manifest.json'), 'utf8')) as {
|
|
84
|
-
|
|
132
|
+
const manifest = JSON.parse(readFileSync(join(destination, 'manifest.json'), 'utf8')) as {
|
|
133
|
+
manifestVersion?: number
|
|
134
|
+
name?: string
|
|
135
|
+
slug?: string
|
|
136
|
+
}
|
|
137
|
+
return manifest.manifestVersion === 1 &&
|
|
138
|
+
manifest.name === name &&
|
|
139
|
+
manifest.slug === slug
|
|
85
140
|
} catch { return false }
|
|
86
141
|
}
|
|
87
142
|
|
|
143
|
+
function removePublishedBackups(
|
|
144
|
+
paths: string[],
|
|
145
|
+
remove: typeof rmSync
|
|
146
|
+
) {
|
|
147
|
+
let firstError: unknown = null
|
|
148
|
+
for (const path of paths) {
|
|
149
|
+
if (!existsSync(path)) continue
|
|
150
|
+
try {
|
|
151
|
+
remove(path, { recursive: true, force: true })
|
|
152
|
+
} catch (error) {
|
|
153
|
+
firstError ??= error
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (firstError) throw new Error('instruction_set_sync_cleanup_failed')
|
|
157
|
+
}
|
|
158
|
+
|
|
88
159
|
export function syncToDisk(out: string, set: SyncSet, fileOps: SyncFileOps = { writeFileSync }) {
|
|
89
|
-
|
|
160
|
+
const directoryName = set.slug || set.name
|
|
161
|
+
safeSegment(directoryName)
|
|
162
|
+
const hasProjectId = set.projectId !== undefined
|
|
163
|
+
const hasInstructionSetId = set.instructionSetId !== undefined
|
|
164
|
+
if (set.slug && hasProjectId !== hasInstructionSetId) {
|
|
165
|
+
throw new Error('instruction_set_sync_identity_missing')
|
|
166
|
+
}
|
|
90
167
|
for (const key of set.shape) safeSegment(key)
|
|
91
168
|
const profileSlugs = new Set<string>()
|
|
92
169
|
for (const profile of set.profiles) {
|
|
@@ -96,11 +173,28 @@ export function syncToDisk(out: string, set: SyncSet, fileOps: SyncFileOps = { w
|
|
|
96
173
|
profileSlugs.add(value)
|
|
97
174
|
}
|
|
98
175
|
mkdirSync(out, { recursive: true })
|
|
99
|
-
const destination = join(out,
|
|
100
|
-
|
|
101
|
-
|
|
176
|
+
const destination = join(out, directoryName)
|
|
177
|
+
const legacyDestination = set.slug && set.name !== set.slug && isSafeSegment(set.name)
|
|
178
|
+
? join(out, set.name)
|
|
179
|
+
: null
|
|
180
|
+
if (existsSync(destination)) {
|
|
181
|
+
if (set.slug && existingDestinationIsLegacySynced(destination, set.name)) {
|
|
182
|
+
throw new Error('instruction_set_sync_legacy_identity_required')
|
|
183
|
+
}
|
|
184
|
+
if (!existingDestinationIsSynced(destination, set)) {
|
|
185
|
+
throw new Error('instruction_set_sync_destination_not_synced')
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (legacyDestination && existsSync(legacyDestination)) {
|
|
189
|
+
if (existingDestinationIsLegacySynced(legacyDestination, set.name)) {
|
|
190
|
+
throw new Error('instruction_set_sync_legacy_identity_required')
|
|
191
|
+
}
|
|
192
|
+
throw new Error('instruction_set_sync_destination_not_synced')
|
|
193
|
+
}
|
|
194
|
+
const priorDestinations = [destination]
|
|
195
|
+
const temp = join(out, `.${directoryName}.${randomUUID()}.tmp`)
|
|
102
196
|
const move = fileOps.renameSync || renameSync
|
|
103
|
-
|
|
197
|
+
const backups: Array<{ original: string; backup: string }> = []
|
|
104
198
|
mkdirSync(temp)
|
|
105
199
|
try {
|
|
106
200
|
const defaultMaterial = writeMaterial(temp, set.default, set.shape, 'default', fileOps)
|
|
@@ -114,18 +208,40 @@ export function syncToDisk(out: string, set: SyncSet, fileOps: SyncFileOps = { w
|
|
|
114
208
|
? Object.entries(profile.production.files).map(([key, path]) => ({ key, modelConfig: profile.modelConfigIdentity, path, syncedContentSha256: profile.production!.syncedContentSha256[key] }))
|
|
115
209
|
: []),
|
|
116
210
|
]
|
|
117
|
-
const manifest = {
|
|
211
|
+
const manifest = {
|
|
212
|
+
manifestVersion: 1,
|
|
213
|
+
...(set.projectId ? { projectId: set.projectId } : {}),
|
|
214
|
+
...(set.instructionSetId ? { instructionSetId: set.instructionSetId } : {}),
|
|
215
|
+
name: set.name,
|
|
216
|
+
...(set.slug ? { slug: set.slug } : {}),
|
|
217
|
+
...(Object.hasOwn(set, 'description') ? { description: set.description ?? null } : {}),
|
|
218
|
+
shape: set.shape,
|
|
219
|
+
components,
|
|
220
|
+
default: defaultMaterial,
|
|
221
|
+
profiles,
|
|
222
|
+
...(set.filteredTo ? { filteredTo: [...set.filteredTo].sort() } : {}),
|
|
223
|
+
}
|
|
118
224
|
fileOps.writeFileSync(join(temp, 'manifest.json'), stable(manifest))
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
225
|
+
for (const prior of priorDestinations) {
|
|
226
|
+
if (!existsSync(prior)) continue
|
|
227
|
+
const backup = join(out, `.${directoryName}.${randomUUID()}.bak`)
|
|
228
|
+
move(prior, backup)
|
|
229
|
+
backups.push({ original: prior, backup })
|
|
230
|
+
}
|
|
231
|
+
move(temp, destination)
|
|
123
232
|
} catch (error) {
|
|
124
233
|
removeOwnScratch([temp])
|
|
125
|
-
|
|
126
|
-
|
|
234
|
+
for (const { original, backup } of [...backups].reverse()) {
|
|
235
|
+
if (existsSync(backup) && !existsSync(original)) move(backup, original)
|
|
236
|
+
}
|
|
237
|
+
removeOwnScratch(backups.map(entry => entry.backup))
|
|
127
238
|
throw error
|
|
128
239
|
}
|
|
240
|
+
removePublishedBackups(
|
|
241
|
+
backups.map(entry => entry.backup),
|
|
242
|
+
fileOps.rmSync || rmSync
|
|
243
|
+
)
|
|
244
|
+
return destination
|
|
129
245
|
}
|
|
130
246
|
|
|
131
247
|
export async function instructionSetsCommand(args: string[], io: InstructionSetsCommandIo): Promise<number> {
|
|
@@ -143,6 +259,44 @@ export async function instructionSetsCommand(args: string[], io: InstructionSets
|
|
|
143
259
|
writeManifest = loadInstructionSetManifest(reference)
|
|
144
260
|
}
|
|
145
261
|
const project = await io.resolveProjectSlug(argValue(args, '--project'))
|
|
262
|
+
if (subcommand === 'scorers') {
|
|
263
|
+
const action = reference
|
|
264
|
+
const set = positionals[2]
|
|
265
|
+
const componentKey = argValue(args, '--key')
|
|
266
|
+
const scorerVersionId = argValue(args, '--scorer-version')
|
|
267
|
+
if (
|
|
268
|
+
(action !== 'set-headline' && action !== 'add') ||
|
|
269
|
+
!set ||
|
|
270
|
+
!componentKey ||
|
|
271
|
+
!scorerVersionId
|
|
272
|
+
) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
'Usage: orizu instructions scorers set-headline|add <set> --key <component-key> --scorer-version <id> --project <team/project> [--dataset-version <id> --split-set <id> --split <name>] [--json]'
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
const payload = await responsePayload(await authedFetch(
|
|
278
|
+
`/api/cli/instruction-sets/${encodeURIComponent(set)}/scorers?project=${encodeURIComponent(project)}`,
|
|
279
|
+
{
|
|
280
|
+
method: 'POST',
|
|
281
|
+
headers: { 'content-type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({
|
|
283
|
+
componentKey,
|
|
284
|
+
scorerVersionId,
|
|
285
|
+
role: action === 'set-headline' ? 'headline' : 'tracked',
|
|
286
|
+
datasetVersionId: argValue(args, '--dataset-version') || undefined,
|
|
287
|
+
splitSetId: argValue(args, '--split-set') || undefined,
|
|
288
|
+
splitName: argValue(args, '--split') || undefined,
|
|
289
|
+
}),
|
|
290
|
+
}
|
|
291
|
+
), `Instruction sets scorers ${action}`)
|
|
292
|
+
if (io.json) io.print(JSON.stringify(payload))
|
|
293
|
+
else {
|
|
294
|
+
io.print(
|
|
295
|
+
`${action === 'set-headline' ? 'Set headline' : 'Added'} scorer ${sanitizeTerminalText(scorerVersionId)} for ${sanitizeTerminalText(set)} / ${sanitizeTerminalText(componentKey)}`
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
return 0
|
|
299
|
+
}
|
|
146
300
|
if (subcommand === 'default') {
|
|
147
301
|
const operation = reference; const set = positionals[2]; const identity = argValue(args, '--model-config'); const version = argValue(args, '--version')
|
|
148
302
|
if ((operation !== 'show' && operation !== 'move') || !set) throw new Error('Usage: orizu instructions default show|move <set> --project <team/project> [--model-config <identity> --version <n>] [--json]')
|
|
@@ -270,8 +424,8 @@ export async function instructionSetsCommand(args: string[], io: InstructionSets
|
|
|
270
424
|
if (!set || (set.slug !== reference && set.name !== reference) || (modelConfig && (!Array.isArray(set.filteredTo) || set.filteredTo.length !== 1 || set.filteredTo[0] !== modelConfig))) {
|
|
271
425
|
throw new Error('instruction_set_sync_response_mismatch')
|
|
272
426
|
}
|
|
273
|
-
syncToDisk(output, set)
|
|
274
|
-
if (io.json) io.print(JSON.stringify(payload)); else io.print(`Synced ${set.name} to ${
|
|
427
|
+
const destination = syncToDisk(output, set)
|
|
428
|
+
if (io.json) io.print(JSON.stringify(payload)); else io.print(`Synced ${set.name} to ${destination}`)
|
|
275
429
|
return 0
|
|
276
430
|
}
|
|
277
431
|
if (subcommand === 'show' && (!reference || reference.startsWith('--'))) throw new Error('Instruction set name is required')
|