orizu 0.5.18 → 0.5.20

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.
Files changed (144) hide show
  1. package/README.md +23 -0
  2. package/dist/diff-comments-cli.js +68 -0
  3. package/dist/gepa-engine-dispatch.js +261 -0
  4. package/dist/gepa-python-command.js +26 -0
  5. package/dist/gepa-python-paths.js +39 -0
  6. package/dist/help.js +4 -4
  7. package/dist/index.js +37 -63
  8. package/dist/markdown-report-input.js +15 -1
  9. package/dist/prompt-length-wire.js +30 -0
  10. package/dist/prompt-report-cli.js +2 -2
  11. package/dist/prompt-summary-table.js +37 -0
  12. package/dist/report-markdown-blankness.js +6 -0
  13. package/dist/report-replacement-warning.js +31 -0
  14. package/package.json +2 -2
  15. package/scripts/build-cli-dist.mjs +30 -0
  16. package/scripts/orizu-source-checkout.d.mts +1 -0
  17. package/scripts/orizu-source-checkout.mjs +10 -0
  18. package/scripts/vendor-gepa-python.mjs +187 -4
  19. package/src/diff-comments-cli.ts +125 -0
  20. package/src/gepa-engine-dispatch.ts +300 -0
  21. package/src/gepa-python-command.ts +31 -0
  22. package/src/gepa-python-paths.ts +53 -0
  23. package/src/help.ts +4 -4
  24. package/src/index.ts +46 -69
  25. package/src/markdown-report-input.ts +19 -1
  26. package/src/prompt-length-wire.ts +65 -0
  27. package/src/prompt-report-cli.ts +2 -2
  28. package/src/prompt-summary-table.ts +57 -0
  29. package/src/report-markdown-blankness.ts +6 -0
  30. package/src/report-replacement-warning.ts +35 -0
  31. package/vendor/gepa-python/LICENSE +7 -0
  32. package/vendor/gepa-python/manifest.json +107 -0
  33. package/vendor/gepa-python/src/gepa/__init__.py +22 -0
  34. package/vendor/gepa-python/src/gepa/adapters/__init__.py +0 -0
  35. package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/__init__.py +1 -0
  36. package/vendor/gepa-python/src/gepa/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
  37. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/__init__.py +26 -0
  38. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/confidence_adapter.py +618 -0
  39. package/vendor/gepa-python/src/gepa/adapters/confidence_adapter/scoring.py +206 -0
  40. package/vendor/gepa-python/src/gepa/adapters/default_adapter/__init__.py +0 -0
  41. package/vendor/gepa-python/src/gepa/adapters/default_adapter/default_adapter.py +206 -0
  42. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/__init__.py +0 -0
  43. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/dspy_adapter.py +525 -0
  44. package/vendor/gepa-python/src/gepa/adapters/dspy_adapter/instruction_proposal.py +500 -0
  45. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/__init__.py +6 -0
  46. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
  47. package/vendor/gepa-python/src/gepa/adapters/dspy_full_program_adapter/full_program_adapter.py +269 -0
  48. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/__init__.py +56 -0
  49. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
  50. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
  51. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
  52. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
  53. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
  54. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
  55. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
  56. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
  57. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
  58. package/vendor/gepa-python/src/gepa/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
  59. package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/__init__.py +11 -0
  60. package/vendor/gepa-python/src/gepa/adapters/langchain_adapter/langchain_adapter.py +261 -0
  61. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/__init__.py +37 -0
  62. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_adapter.py +693 -0
  63. package/vendor/gepa-python/src/gepa/adapters/mcp_adapter/mcp_client.py +364 -0
  64. package/vendor/gepa-python/src/gepa/adapters/optimize_anything_adapter/optimize_anything_adapter.py +879 -0
  65. package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/__init__.py +0 -0
  66. package/vendor/gepa-python/src/gepa/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
  67. package/vendor/gepa-python/src/gepa/api.py +468 -0
  68. package/vendor/gepa-python/src/gepa/core/__init__.py +0 -0
  69. package/vendor/gepa-python/src/gepa/core/adapter.py +227 -0
  70. package/vendor/gepa-python/src/gepa/core/callbacks.py +551 -0
  71. package/vendor/gepa-python/src/gepa/core/data_loader.py +74 -0
  72. package/vendor/gepa-python/src/gepa/core/engine.py +1025 -0
  73. package/vendor/gepa-python/src/gepa/core/result.py +271 -0
  74. package/vendor/gepa-python/src/gepa/core/state.py +706 -0
  75. package/vendor/gepa-python/src/gepa/examples/__init__.py +0 -0
  76. package/vendor/gepa-python/src/gepa/examples/aime.py +24 -0
  77. package/vendor/gepa-python/src/gepa/examples/anymaths-bench/eval_default.py +111 -0
  78. package/vendor/gepa-python/src/gepa/examples/anymaths-bench/train_anymaths.py +177 -0
  79. package/vendor/gepa-python/src/gepa/examples/mcp_adapter/__init__.py +4 -0
  80. package/vendor/gepa-python/src/gepa/examples/mcp_adapter/mcp_optimization_example.py +456 -0
  81. package/vendor/gepa-python/src/gepa/examples/rag_adapter/__init__.py +9 -0
  82. package/vendor/gepa-python/src/gepa/examples/rag_adapter/rag_optimization.py +824 -0
  83. package/vendor/gepa-python/src/gepa/examples/terminal-bench/train_terminus.py +161 -0
  84. package/vendor/gepa-python/src/gepa/gepa_utils.py +117 -0
  85. package/vendor/gepa-python/src/gepa/gskill/__init__.py +1 -0
  86. package/vendor/gepa-python/src/gepa/gskill/gskill/__init__.py +0 -0
  87. package/vendor/gepa-python/src/gepa/gskill/gskill/cost_tracker.py +194 -0
  88. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/__init__.py +0 -0
  89. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code.py +1117 -0
  90. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/claude_code_skills.py +613 -0
  91. package/vendor/gepa-python/src/gepa/gskill/gskill/evaluate/mini_swe_agent.py +440 -0
  92. package/vendor/gepa-python/src/gepa/gskill/gskill/experiment_logger.py +367 -0
  93. package/vendor/gepa-python/src/gepa/gskill/gskill/swe_fitness_fn.py +209 -0
  94. package/vendor/gepa-python/src/gepa/gskill/gskill/swe_harness.py +352 -0
  95. package/vendor/gepa-python/src/gepa/gskill/gskill/train_optimize_anything.py +830 -0
  96. package/vendor/gepa-python/src/gepa/image.py +104 -0
  97. package/vendor/gepa-python/src/gepa/lm.py +261 -0
  98. package/vendor/gepa-python/src/gepa/logging/__init__.py +0 -0
  99. package/vendor/gepa-python/src/gepa/logging/experiment_tracker.py +457 -0
  100. package/vendor/gepa-python/src/gepa/logging/logger.py +75 -0
  101. package/vendor/gepa-python/src/gepa/logging/utils.py +130 -0
  102. package/vendor/gepa-python/src/gepa/optimize_anything.py +1701 -0
  103. package/vendor/gepa-python/src/gepa/proposer/__init__.py +0 -0
  104. package/vendor/gepa-python/src/gepa/proposer/base.py +53 -0
  105. package/vendor/gepa-python/src/gepa/proposer/merge.py +403 -0
  106. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/__init__.py +0 -0
  107. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/base.py +64 -0
  108. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflection_lm.py +178 -0
  109. package/vendor/gepa-python/src/gepa/proposer/reflective_mutation/reflective_mutation.py +641 -0
  110. package/vendor/gepa-python/src/gepa/py.typed +0 -0
  111. package/vendor/gepa-python/src/gepa/strategies/__init__.py +0 -0
  112. package/vendor/gepa-python/src/gepa/strategies/acceptance.py +66 -0
  113. package/vendor/gepa-python/src/gepa/strategies/batch_sampler.py +103 -0
  114. package/vendor/gepa-python/src/gepa/strategies/candidate_selector.py +82 -0
  115. package/vendor/gepa-python/src/gepa/strategies/component_selector.py +36 -0
  116. package/vendor/gepa-python/src/gepa/strategies/eval_policy.py +64 -0
  117. package/vendor/gepa-python/src/gepa/strategies/instruction_proposal.py +153 -0
  118. package/vendor/gepa-python/src/gepa/strategies/proposal_sampling.py +128 -0
  119. package/vendor/gepa-python/src/gepa/strategies/proposal_selection.py +84 -0
  120. package/vendor/gepa-python/src/gepa/utils/__init__.py +63 -0
  121. package/vendor/gepa-python/src/gepa/utils/code_execution.py +626 -0
  122. package/vendor/gepa-python/src/gepa/utils/stdio_capture.py +145 -0
  123. package/vendor/gepa-python/src/gepa/utils/stop_condition.py +230 -0
  124. package/vendor/gepa-python/src/gepa/visualization.py +359 -0
  125. package/vendor/orizu-gepa/pyproject.toml +18 -0
  126. package/vendor/orizu-gepa/src/orizu_gepa_connector/__init__.py +8 -0
  127. package/vendor/orizu-gepa/src/orizu_gepa_connector/__main__.py +5 -0
  128. package/vendor/orizu-gepa/src/orizu_gepa_connector/adapter.py +132 -0
  129. package/vendor/orizu-gepa/src/orizu_gepa_connector/callbacks.py +652 -0
  130. package/vendor/orizu-gepa/src/orizu_gepa_connector/cli.py +7 -0
  131. package/vendor/orizu-gepa/src/orizu_gepa_connector/engine.py +177 -0
  132. package/vendor/orizu-gepa/src/orizu_gepa_connector/preflight.py +64 -0
  133. package/vendor/orizu-gepa/src/orizu_gepa_connector/reflection.py +106 -0
  134. package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +643 -0
  135. package/vendor/orizu-gepa/src/orizu_gepa_connector/stop_conditions.py +38 -0
  136. package/vendor/orizu-gepa/src/orizu_gepa_connector/translator.py +202 -0
  137. package/vendor/orizu-gepa-python/pyproject.toml +1 -1
  138. package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +1 -0
  139. package/vendor/orizu-gepa-python/src/orizu_gepa/local_log.py +49 -0
  140. package/vendor/skills/orizu-cli/SKILL.md +1 -1
  141. package/vendor/skills/orizu-cli/references/cli-reference.md +10 -0
  142. package/vendor/skills/orizu-cli/references/optimization-reports.md +1 -1
  143. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +1 -1
  144. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +23 -1
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { basename } from 'path';
3
+ import { hasCanonicalReportMarkdownContent } from './report-markdown-blankness.js';
3
4
  export const MARKDOWN_REPORT_MAX_BYTES = 2 * 1024 * 1024;
4
5
  function getArg(args, name) {
5
6
  const index = args.indexOf(name);
@@ -17,6 +18,11 @@ function rejectDashPrefixedOptionValue(name, value) {
17
18
  throw new Error(`Invalid value for ${name}: option values cannot start with a dash`);
18
19
  }
19
20
  }
21
+ function rejectMissingOptionValue(args, name) {
22
+ if (args.some((arg, index) => arg === name && index + 1 >= args.length)) {
23
+ throw new Error(`${name} requires a value`);
24
+ }
25
+ }
20
26
  function readSourceFile(pathArg) {
21
27
  const expandedPath = expandHomePath(pathArg);
22
28
  try {
@@ -31,6 +37,14 @@ function readSourceFile(pathArg) {
31
37
  }
32
38
  }
33
39
  export function readMarkdownReportInput(args, reportLabel) {
40
+ if (args.some(arg => arg.startsWith('--report='))) {
41
+ throw new Error('use --report <markdown|@file> with a space, not =');
42
+ }
43
+ if (args.some(arg => arg.startsWith('--report-file='))) {
44
+ throw new Error('use --report-file <path> with a space, not =');
45
+ }
46
+ rejectMissingOptionValue(args, '--report');
47
+ rejectMissingOptionValue(args, '--report-file');
34
48
  const report = getArg(args, '--report');
35
49
  const reportFile = getArg(args, '--report-file');
36
50
  rejectDashPrefixedOptionValue('--report', report);
@@ -57,7 +71,7 @@ export function readMarkdownReportInput(args, reportLabel) {
57
71
  markdown = report || '';
58
72
  sourceName = 'inline';
59
73
  }
60
- if (!markdown.trim()) {
74
+ if (!hasCanonicalReportMarkdownContent(markdown)) {
61
75
  throw new Error(`${reportLabel} report markdown must not be blank`);
62
76
  }
63
77
  if (Buffer.byteLength(markdown, 'utf8') > MARKDOWN_REPORT_MAX_BYTES) {
@@ -0,0 +1,30 @@
1
+ export const CLI_LENGTH_STATS_FIELDS = [
2
+ 'tokens',
3
+ 'lines',
4
+ 'chars',
5
+ 'words',
6
+ ];
7
+ export const CLI_LENGTH_DELTA_SPLIT_UNAVAILABLE_REASONS = [
8
+ 'diff_degraded_size_limit',
9
+ 'diff_degraded_cell_limit',
10
+ ];
11
+ export const CLI_LENGTH_MEASUREMENT_UNAVAILABLE_REASONS = [
12
+ 'length_measurement_size_limit',
13
+ 'length_measurement_failed',
14
+ 'body_resolution_failed',
15
+ 'measurement_cap_exceeded',
16
+ 'enrichment_failed',
17
+ 'bodies_unavailable_event_cap',
18
+ ];
19
+ export function formatFiniteLengthCount(value, signed = false) {
20
+ if (typeof value !== 'number' || !Number.isFinite(value))
21
+ return null;
22
+ if (!signed)
23
+ return value.toLocaleString('en-US');
24
+ const formatted = Math.abs(value).toLocaleString('en-US');
25
+ if (value > 0)
26
+ return `+${formatted}`;
27
+ if (value < 0)
28
+ return `-${formatted}`;
29
+ return formatted;
30
+ }
@@ -27,7 +27,7 @@ export async function promptReportCommand(args, context) {
27
27
  rejectDashPrefixedOptionValue(name, value);
28
28
  }
29
29
  if (!prompt) {
30
- throw new Error('Usage: orizu prompts report set --prompt <id-or-name> --project <team/project> ' +
30
+ throw new Error('Usage: orizu prompts report set --prompt <id-or-name> [--project <team/project>] ' +
31
31
  '(--version <n> | --version-id <id>) ' +
32
32
  '(--report <markdown|@file> | --report-file <path>) [--json]');
33
33
  }
@@ -39,7 +39,7 @@ export async function promptReportCommand(args, context) {
39
39
  }
40
40
  const report = readMarkdownReportInput(args, 'Prompt');
41
41
  if (!report) {
42
- throw new Error('Usage: orizu prompts report set --prompt <id-or-name> --project <team/project> ' +
42
+ throw new Error('Usage: orizu prompts report set --prompt <id-or-name> [--project <team/project>] ' +
43
43
  '(--version <n> | --version-id <id>) ' +
44
44
  '(--report <markdown|@file> | --report-file <path>) [--json]');
45
45
  }
@@ -0,0 +1,37 @@
1
+ import { sanitizeTerminalText } from './json-response.js';
2
+ import { formatFiniteLengthCount, } from './prompt-length-wire.js';
3
+ export function printPromptSummaryTable(items, emptyMessage, printLine) {
4
+ if (items.length === 0) {
5
+ printLine(emptyMessage);
6
+ return;
7
+ }
8
+ const rows = items.map(item => ({
9
+ id: sanitizeTerminalText(item.id),
10
+ name: sanitizeTerminalText(item.name),
11
+ role: sanitizeTerminalText(item.role),
12
+ status: sanitizeTerminalText(item.status || 'active'),
13
+ tokens: item.lengthStats == null
14
+ ? '—'
15
+ : (() => {
16
+ const count = formatFiniteLengthCount(item.lengthStats.tokens);
17
+ return count === null ? '—' : `~${count}`;
18
+ })(),
19
+ lines: item.lengthStats == null
20
+ ? '—'
21
+ : formatFiniteLengthCount(item.lengthStats.lines) ?? '—',
22
+ chars: item.lengthStats == null
23
+ ? '—'
24
+ : formatFiniteLengthCount(item.lengthStats.chars) ?? '—',
25
+ words: item.lengthStats == null
26
+ ? '—'
27
+ : formatFiniteLengthCount(item.lengthStats.words) ?? '—',
28
+ }));
29
+ const headers = ['ID', 'NAME', 'ROLE', 'STATUS', 'TOKENS', 'LINES', 'CHARS', 'WORDS'];
30
+ const keys = ['id', 'name', 'role', 'status', 'tokens', 'lines', 'chars', 'words'];
31
+ const widths = keys.map((key, index) => Math.max(headers[index].length, ...rows.map(row => row[key].length)));
32
+ printLine(headers.map((header, index) => header.padEnd(widths[index])).join(' '));
33
+ printLine(widths.map(width => '-'.repeat(width)).join(' '));
34
+ rows.forEach(row => {
35
+ printLine(keys.map((key, index) => row[key].padEnd(widths[index])).join(' '));
36
+ });
37
+ }
@@ -0,0 +1,6 @@
1
+ // JavaScript's \s is broader than the database locale's POSIX [:space:]
2
+ // predicate. These four code points are content in the measured PostgreSQL
3
+ // boundary, so retain them explicitly when validating CLI report input.
4
+ export function hasCanonicalReportMarkdownContent(markdown) {
5
+ return /(?:[^\s]|[\u00a0\u2007\u202f\ufeff])/u.test(markdown);
6
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * ALI-1452: a shared, human-readable warning for a replaced prompt-version
3
+ * report on a push/draft response.
4
+ *
5
+ * Re-pushing an unchanged prompt with a report deliberately replaces the
6
+ * version's existing report (reports are correctable; versions are immutable).
7
+ * The server discloses that replacement as `report_replaced:
8
+ * { source_name, updated_at } | null`; this turns a non-null disclosure into
9
+ * the stderr warning line the push commands print. JSON mode prints nothing —
10
+ * the payload already carries `report_replaced`.
11
+ */
12
+ function isRecord(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+ /**
16
+ * A one-line warning when a push response discloses that it replaced an
17
+ * existing report, or null when nothing was replaced (or no report was sent).
18
+ */
19
+ export function reportReplacementWarning(data) {
20
+ const replaced = data.report_replaced;
21
+ if (!isRecord(replaced))
22
+ return null;
23
+ const sourceName = typeof replaced.source_name === 'string'
24
+ ? replaced.source_name
25
+ : 'unknown source';
26
+ const updatedAt = typeof replaced.updated_at === 'string'
27
+ ? replaced.updated_at
28
+ : 'unknown time';
29
+ return ('Warning: replaced the existing report for this version ' +
30
+ `(${sourceName}, last updated ${updatedAt})`);
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.5.18",
3
+ "version": "0.5.20",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "build": "tsc -p tsconfig.json",
19
- "prepack": "node scripts/vendor-gepa-python.mjs && node scripts/vendor-orizu-skill.mjs",
19
+ "prepack": "node scripts/build-cli-dist.mjs && node scripts/vendor-gepa-python.mjs && node scripts/vendor-orizu-skill.mjs",
20
20
  "postpack": "node scripts/vendor-gepa-python.mjs --clean && node scripts/vendor-orizu-skill.mjs --clean"
21
21
  },
22
22
  "dependencies": {
@@ -0,0 +1,30 @@
1
+ import { existsSync, rmSync } from 'node:fs'
2
+ import { spawnSync } from 'node:child_process'
3
+ import { dirname, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import { isOrizuSourceCheckout } from './orizu-source-checkout.mjs'
7
+
8
+ const scriptDir = dirname(fileURLToPath(import.meta.url))
9
+ const cliRoot = resolve(scriptDir, '..')
10
+ const repoRoot = resolve(cliRoot, '..', '..')
11
+ const packedEntrypoint = resolve(cliRoot, 'dist', 'index.js')
12
+
13
+ // A source checkout has the compiler and must always rebuild before packing so
14
+ // ignored dist cannot carry an old CLI. A published tarball has no dev compiler;
15
+ // it may be repacked only when it already carries the built entrypoint.
16
+ if (!isOrizuSourceCheckout(resolve(repoRoot, 'package.json'))) {
17
+ if (!existsSync(packedEntrypoint)) {
18
+ throw new Error('Unable to pack CLI: published package is missing dist/index.js')
19
+ }
20
+ process.exit(0)
21
+ }
22
+
23
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
24
+ // Delete old compiler output first: npm pack must never retain a module whose
25
+ // TypeScript source was renamed or removed since the previous build.
26
+ rmSync(resolve(cliRoot, 'dist'), { recursive: true, force: true })
27
+ const result = spawnSync(npm, ['run', 'build'], { cwd: cliRoot, stdio: 'inherit' })
28
+ if (result.status !== 0) {
29
+ throw new Error(`Unable to build CLI dist before packing: ${result.error?.message || 'npm run build failed'}`)
30
+ }
@@ -0,0 +1 @@
1
+ export function isOrizuSourceCheckout(packageJsonPath: string): boolean
@@ -0,0 +1,10 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ /** True only for the Orizu monorepo, never merely for a consumer Git checkout. */
4
+ export function isOrizuSourceCheckout(packageJsonPath) {
5
+ try {
6
+ return JSON.parse(readFileSync(packageJsonPath, 'utf8')).name === 'orizu'
7
+ } catch {
8
+ return false
9
+ }
10
+ }
@@ -1,4 +1,6 @@
1
- import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync, readFileSync } from 'node:fs'
2
+ import { createHash } from 'node:crypto'
3
+ import { spawnSync } from 'node:child_process'
2
4
  import { dirname, resolve } from 'node:path'
3
5
  import { fileURLToPath } from 'node:url'
4
6
 
@@ -7,32 +9,194 @@ const cliRoot = resolve(scriptDir, '..')
7
9
  const repoRoot = resolve(cliRoot, '..', '..')
8
10
  const sourceRoot = resolve(repoRoot, 'packages', 'orizu-gepa-python')
9
11
  const vendorRoot = resolve(cliRoot, 'vendor', 'orizu-gepa-python')
12
+ const connectorSourceRoot = resolve(repoRoot, 'packages', 'orizu-gepa')
13
+ const connectorVendorRoot = resolve(cliRoot, 'vendor', 'orizu-gepa')
14
+ const officialArchive = resolve(cliRoot, 'gepa-python-source.zip')
15
+ const officialVendorRoot = resolve(cliRoot, 'vendor', 'gepa-python')
10
16
  const shouldClean = process.argv.includes('--clean')
17
+ const shouldRefresh = process.argv.includes('--refresh')
18
+ const archiveArgumentIndex = process.argv.indexOf('--archive')
19
+ const suppliedArchive = archiveArgumentIndex === -1
20
+ ? null
21
+ : process.argv[archiveArgumentIndex + 1]
11
22
  const sourceSrc = resolve(sourceRoot, 'src')
12
23
  const sourcePyproject = resolve(sourceRoot, 'pyproject.toml')
13
24
  const sourceManifest = resolve(sourceRoot, 'manifest.json')
14
25
  const vendoredSrc = resolve(vendorRoot, 'src')
15
26
  const vendoredPyproject = resolve(vendorRoot, 'pyproject.toml')
16
27
  const vendoredManifest = resolve(vendorRoot, 'manifest.json')
28
+ const officialVendoredManifest = resolve(officialVendorRoot, 'manifest.json')
29
+ const officialVendoredGepa = resolve(officialVendorRoot, 'src', 'gepa', '__init__.py')
30
+ const GEPA_VERSION = '0.1.4'
31
+ const GEPA_RELEASE_COMMIT = '8b0ce6cd99a234f6b74daf37558a2ac0ce18f975'
32
+ const GEPA_WHEEL_FILENAME = 'gepa-0.1.4-py3-none-any.whl'
33
+ const GEPA_WHEEL_SHA256 = '12b971039599625c156d2231f6d72a29c31a22e9c237689459b5f1a3c353f532'
17
34
  const hasSourcePackage = existsSync(sourceSrc)
18
35
  && existsSync(sourcePyproject)
19
36
  && existsSync(sourceManifest)
37
+ const hasConnectorSourcePackage = existsSync(resolve(connectorSourceRoot, 'src'))
38
+ && existsSync(resolve(connectorSourceRoot, 'pyproject.toml'))
20
39
  const hasVendoredPackage = existsSync(vendoredSrc)
21
40
  && existsSync(vendoredPyproject)
22
41
  && existsSync(vendoredManifest)
42
+ const hasVendoredConnectorPackage = existsSync(resolve(connectorVendorRoot, 'src', 'orizu_gepa_connector', '__main__.py'))
43
+ && existsSync(resolve(connectorVendorRoot, 'pyproject.toml'))
44
+ const hasOfficialVendoredPackage = existsSync(officialVendoredManifest)
45
+ && existsSync(officialVendoredGepa)
46
+
47
+ function sha256File(path) {
48
+ return createHash('sha256').update(readFileSync(path)).digest('hex')
49
+ }
50
+
51
+ function assertPinnedGepaArchive(path) {
52
+ if (!existsSync(path)) {
53
+ throw new Error(`Unable to vendor official GEPA: source archive not found at ${path}`)
54
+ }
55
+
56
+ const actualSha256 = sha256File(path)
57
+ if (actualSha256 !== GEPA_WHEEL_SHA256) {
58
+ throw new Error(
59
+ `Official GEPA source archive sha256 mismatch: expected ${GEPA_WHEEL_SHA256}, got ${actualSha256}`
60
+ )
61
+ }
62
+ }
63
+
64
+ function collectFileHashes(root, prefix = '') {
65
+ const files = {}
66
+ for (const entry of readdirSync(root).sort()) {
67
+ const path = resolve(root, entry)
68
+ const relativePath = prefix ? `${prefix}/${entry}` : entry
69
+ if (statSync(path).isDirectory()) {
70
+ Object.assign(files, collectFileHashes(path, relativePath))
71
+ } else {
72
+ files[relativePath] = sha256File(path)
73
+ }
74
+ }
75
+ return files
76
+ }
77
+
78
+ function assertOfficialVendoredManifest() {
79
+ let manifest
80
+ try {
81
+ manifest = JSON.parse(readFileSync(officialVendoredManifest, 'utf8'))
82
+ } catch (error) {
83
+ throw new Error(`Official GEPA vendored manifest is unreadable: ${error.message}`)
84
+ }
85
+
86
+ if (!manifest.files || typeof manifest.files !== 'object' || Array.isArray(manifest.files)) {
87
+ throw new Error('Official GEPA vendored manifest is missing file hashes')
88
+ }
89
+
90
+ const actualFiles = collectFileHashes(officialVendorRoot)
91
+ delete actualFiles['manifest.json']
92
+ const expectedEntries = Object.entries(manifest.files).sort(([left], [right]) => left.localeCompare(right))
93
+ const actualEntries = Object.entries(actualFiles).sort(([left], [right]) => left.localeCompare(right))
94
+ if (expectedEntries.length !== actualEntries.length) {
95
+ throw new Error('Official GEPA vendored manifest file count does not match extracted files')
96
+ }
97
+
98
+ for (let index = 0; index < expectedEntries.length; index += 1) {
99
+ const [expectedPath, expectedSha256] = expectedEntries[index]
100
+ const [actualPath, actualSha256] = actualEntries[index]
101
+ if (expectedPath !== actualPath || expectedSha256 !== actualSha256) {
102
+ throw new Error(`Official GEPA vendored manifest mismatch at ${expectedPath}`)
103
+ }
104
+ }
105
+ }
106
+
107
+ function extractPinnedGepa(archivePath) {
108
+ const extractor = String.raw`
109
+ from pathlib import Path, PurePosixPath
110
+ import shutil
111
+ import sys
112
+ import zipfile
113
+
114
+ archive_path = Path(sys.argv[1])
115
+ destination_root = Path(sys.argv[2])
116
+ license_member = 'gepa-0.1.4.dist-info/licenses/LICENSE'
117
+
118
+ with zipfile.ZipFile(archive_path) as archive:
119
+ for member in archive.infolist():
120
+ if member.is_dir():
121
+ continue
122
+ if member.filename.startswith('gepa/'):
123
+ relative_path = PurePosixPath('src') / PurePosixPath(member.filename)
124
+ elif member.filename == license_member:
125
+ relative_path = PurePosixPath('LICENSE')
126
+ else:
127
+ continue
128
+ if relative_path.is_absolute() or '..' in relative_path.parts:
129
+ raise ValueError(f'unsafe GEPA wheel member: {member.filename}')
130
+ destination = destination_root.joinpath(*relative_path.parts)
131
+ destination.parent.mkdir(parents=True, exist_ok=True)
132
+ with archive.open(member) as source, destination.open('wb') as output:
133
+ shutil.copyfileobj(source, output)
134
+ `
135
+ const result = spawnSync('python3', ['-c', extractor, archivePath, officialVendorRoot], {
136
+ encoding: 'utf8',
137
+ })
138
+ if (result.status !== 0) {
139
+ const details = result.error?.message || result.stderr || result.stdout || 'no subprocess output'
140
+ throw new Error(`Unable to extract verified official GEPA wheel; python3 is required: ${details}`)
141
+ }
142
+ }
143
+
144
+ function vendorOfficialGepa(archivePath) {
145
+ assertPinnedGepaArchive(archivePath)
146
+ rmSync(officialVendorRoot, { recursive: true, force: true })
147
+ mkdirSync(officialVendorRoot, { recursive: true })
148
+ extractPinnedGepa(archivePath)
149
+
150
+ const files = collectFileHashes(officialVendorRoot)
151
+ writeFileSync(officialVendoredManifest, `${JSON.stringify({
152
+ schemaVersion: 'gepa-vendor.v1',
153
+ upstream: {
154
+ package: 'gepa',
155
+ version: GEPA_VERSION,
156
+ repositoryCommit: GEPA_RELEASE_COMMIT,
157
+ wheel: {
158
+ filename: GEPA_WHEEL_FILENAME,
159
+ sha256: GEPA_WHEEL_SHA256,
160
+ },
161
+ },
162
+ files,
163
+ }, null, 2)}\n`)
164
+ }
165
+
166
+ if (archiveArgumentIndex !== -1 && (!suppliedArchive || suppliedArchive.startsWith('--'))) {
167
+ throw new Error('Expected a source archive path after --archive')
168
+ }
169
+
170
+ if (shouldRefresh) {
171
+ if (!suppliedArchive) {
172
+ throw new Error('Refreshing official GEPA requires --archive <verified-wheel-path>')
173
+ }
174
+ assertPinnedGepaArchive(suppliedArchive)
175
+ cpSync(suppliedArchive, officialArchive)
176
+ }
23
177
 
24
178
  if (shouldClean) {
25
179
  if (hasSourcePackage) {
26
180
  rmSync(vendorRoot, { recursive: true, force: true })
27
181
  }
182
+ if (hasConnectorSourcePackage) {
183
+ rmSync(connectorVendorRoot, { recursive: true, force: true })
184
+ }
185
+ if (hasSourcePackage || hasConnectorSourcePackage) {
186
+ rmSync(officialVendorRoot, { recursive: true, force: true })
187
+ }
28
188
  process.exit(0)
29
189
  }
30
190
 
31
- if (!hasSourcePackage) {
32
- if (hasVendoredPackage) {
191
+ if (!hasSourcePackage || !hasConnectorSourcePackage) {
192
+ if (hasVendoredPackage && hasVendoredConnectorPackage && hasOfficialVendoredPackage) {
193
+ assertOfficialVendoredManifest()
33
194
  process.exit(0)
34
195
  }
35
- throw new Error(`Unable to vendor orizu-gepa-python: source package not found at ${sourceRoot}`)
196
+ if (hasVendoredPackage || hasVendoredConnectorPackage || hasOfficialVendoredPackage) {
197
+ throw new Error('Unable to vendor GEPA packages: vendored legacy, connector, or official package is incomplete')
198
+ }
199
+ throw new Error(`Unable to vendor GEPA packages: source package not found at ${sourceRoot} or ${connectorSourceRoot}`)
36
200
  }
37
201
 
38
202
  rmSync(vendorRoot, { recursive: true, force: true })
@@ -52,3 +216,22 @@ cpSync(sourceSrc, resolve(vendorRoot, 'src'), {
52
216
  && !normalizedPath.endsWith('/.DS_Store')
53
217
  },
54
218
  })
219
+
220
+ rmSync(connectorVendorRoot, { recursive: true, force: true })
221
+ mkdirSync(connectorVendorRoot, { recursive: true })
222
+ cpSync(resolve(connectorSourceRoot, 'pyproject.toml'), resolve(connectorVendorRoot, 'pyproject.toml'))
223
+ cpSync(resolve(connectorSourceRoot, 'src'), resolve(connectorVendorRoot, 'src'), {
224
+ recursive: true,
225
+ filter: sourcePath => {
226
+ const normalizedPath = sourcePath.replace(/\\/g, '/')
227
+ return !normalizedPath.endsWith('/__pycache__')
228
+ && !normalizedPath.includes('/__pycache__/')
229
+ && !normalizedPath.endsWith('.pyc')
230
+ && !normalizedPath.endsWith('.pyo')
231
+ && !normalizedPath.endsWith('/.pytest_cache')
232
+ && !normalizedPath.includes('/.pytest_cache/')
233
+ && !normalizedPath.endsWith('/.DS_Store')
234
+ },
235
+ })
236
+
237
+ vendorOfficialGepa(suppliedArchive ?? officialArchive)
@@ -3,6 +3,13 @@ import {
3
3
  isJsonErrorResponsePayload,
4
4
  } from './error-response.js'
5
5
  import { sanitizeHumanInlineText } from './json-response.js'
6
+ import {
7
+ formatFiniteLengthCount,
8
+ type CliLengthDelta,
9
+ type CliLengthMeasurementUnavailableReason,
10
+ type CliLengthStats,
11
+ type CliLengthStatsPair,
12
+ } from './prompt-length-wire.js'
6
13
 
7
14
  interface DiffCommentAuthor {
8
15
  name?: string
@@ -42,6 +49,13 @@ interface DiffCommentPayloadPairContext {
42
49
  diff: string | null
43
50
  bodies: { from: string; to: string } | null
44
51
  degraded: { reason: DiffCommentContextUnavailableReason } | null
52
+ lengthStats: CliLengthStatsPair | null
53
+ lengthDelta: (
54
+ Partial<Record<keyof Omit<CliLengthDelta, 'splitUnavailableReason'>, unknown>> & {
55
+ splitUnavailableReason?: unknown
56
+ }
57
+ ) | null
58
+ lengthDeltaUnavailableReason?: CliLengthMeasurementUnavailableReason
45
59
  comments: DiffCommentPayloadComment[]
46
60
  }
47
61
 
@@ -66,6 +80,12 @@ interface DiffCommentsPayload {
66
80
  pairs: DiffCommentPayloadPair[]
67
81
  }
68
82
 
83
+ interface RenderableLengthMetricDelta {
84
+ removed: number | null
85
+ added: number | null
86
+ net: number | null
87
+ }
88
+
69
89
  export interface DiffCommentsCliContext {
70
90
  getArg: (name: string) => string | null
71
91
  rejectDashPrefixedOptionValue: (name: string, value: string | null) => void
@@ -143,6 +163,110 @@ function humanLineNumber(
143
163
  : sanitizeHumanInlineText(ctx.sanitizeTerminalText, String(line + 1))
144
164
  }
145
165
 
166
+ function formatStats(stats: CliLengthStats): string {
167
+ const tokens = formatFiniteLengthCount(stats.tokens)
168
+ return `${tokens === null ? '—' : `~${tokens}`} tokens · ` +
169
+ `${formatFiniteLengthCount(stats.lines) ?? '—'} lines · ` +
170
+ `${formatFiniteLengthCount(stats.chars) ?? '—'} chars · ` +
171
+ `${formatFiniteLengthCount(stats.words) ?? '—'} words`
172
+ }
173
+
174
+ function formatDeltaValue(
175
+ value: number | null,
176
+ metric: keyof Omit<CliLengthDelta, 'splitUnavailableReason'>,
177
+ signed = false
178
+ ): string {
179
+ const formatted = formatFiniteLengthCount(value, signed)
180
+ if (formatted === null) return '—'
181
+ return metric === 'tokens' ? `~${formatted}` : formatted
182
+ }
183
+
184
+ function isLengthMetricDelta(
185
+ value: unknown
186
+ ): value is RenderableLengthMetricDelta {
187
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
188
+ return false
189
+ }
190
+ const delta = value as Record<string, unknown>
191
+ const isNullableFiniteNumber = (candidate: unknown) =>
192
+ candidate === null || (
193
+ typeof candidate === 'number' && Number.isFinite(candidate)
194
+ )
195
+ return isNullableFiniteNumber(delta.removed) &&
196
+ isNullableFiniteNumber(delta.added) &&
197
+ isNullableFiniteNumber(delta.net)
198
+ }
199
+
200
+ function printLengthMeasurements(
201
+ ctx: DiffCommentsCliContext,
202
+ pair: DiffCommentPayloadPair
203
+ ) {
204
+ function unavailableSuffix(reason: unknown): string {
205
+ if (typeof reason !== 'string' || reason.length === 0) return ''
206
+ return ` (${sanitizeHumanInlineText(ctx.sanitizeTerminalText, reason)})`
207
+ }
208
+
209
+ if (!pair.lengthStats && !pair.lengthDelta) {
210
+ ctx.printLine(
211
+ ` Length: unavailable${unavailableSuffix(
212
+ pair.lengthDeltaUnavailableReason ?? pair.degraded?.reason
213
+ )}`
214
+ )
215
+ return
216
+ }
217
+
218
+ if (pair.lengthStats) {
219
+ ctx.printLine(pair.lengthStats.from
220
+ ? ` From: ${formatStats(pair.lengthStats.from)}`
221
+ : ` From: unavailable${unavailableSuffix(
222
+ pair.lengthStats.fromUnavailableReason
223
+ )}`)
224
+ ctx.printLine(pair.lengthStats.to
225
+ ? ` To: ${formatStats(pair.lengthStats.to)}`
226
+ : ` To: unavailable${unavailableSuffix(
227
+ pair.lengthStats.toUnavailableReason
228
+ )}`)
229
+ }
230
+ if (!pair.lengthDelta) {
231
+ ctx.printLine(
232
+ ` Length delta: unavailable${unavailableSuffix(
233
+ pair.lengthDeltaUnavailableReason ?? pair.degraded?.reason
234
+ )}`
235
+ )
236
+ return
237
+ }
238
+
239
+ const splitUnavailableReason = pair.lengthDelta.splitUnavailableReason
240
+ if (
241
+ typeof splitUnavailableReason === 'string' &&
242
+ splitUnavailableReason.length > 0
243
+ ) {
244
+ ctx.printLine(
245
+ ` Split unavailable: ${sanitizeHumanInlineText(
246
+ ctx.sanitizeTerminalText,
247
+ splitUnavailableReason
248
+ )}`
249
+ )
250
+ }
251
+ for (const [metric, label] of [
252
+ ['tokens', 'Tokens'],
253
+ ['lines', 'Lines'],
254
+ ['chars', 'Chars'],
255
+ ['words', 'Words'],
256
+ ] as const) {
257
+ const delta = pair.lengthDelta[metric]
258
+ if (!isLengthMetricDelta(delta)) {
259
+ ctx.printLine(` ${label}: unavailable`)
260
+ continue
261
+ }
262
+ ctx.printLine(
263
+ ` ${label}: removed ${formatDeltaValue(delta.removed, metric)} · ` +
264
+ `added ${formatDeltaValue(delta.added, metric)} · ` +
265
+ `net ${formatDeltaValue(delta.net, metric, true)}`
266
+ )
267
+ }
268
+ }
269
+
146
270
  function buildParams(ctx: DiffCommentsCliContext): URLSearchParams {
147
271
  const runId = ctx.getArg('--run')
148
272
  const promptId = ctx.getArg('--prompt')
@@ -210,6 +334,7 @@ function printDiffComments(ctx: DiffCommentsCliContext, payload: DiffCommentsPay
210
334
  `${sanitizeHumanInlineText(ctx.sanitizeTerminalText, from)} → ` +
211
335
  `${sanitizeHumanInlineText(ctx.sanitizeTerminalText, to)}`
212
336
  )
337
+ printLengthMeasurements(ctx, pair)
213
338
  if (pair.degraded) {
214
339
  ctx.printLine(
215
340
  ` Context unavailable: ${sanitizeHumanInlineText(ctx.sanitizeTerminalText, pair.degraded.reason)}`