@giamme/forge 0.0.0-stage → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +205 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/SKILL.md +123 -0
- package/VERSION +1 -0
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +69 -0
- package/docs/commands.md +315 -0
- package/docs/dataset.md +20 -0
- package/docs/decomposed-runs.md +268 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +92 -0
- package/docs/fractal.md +181 -0
- package/docs/install.md +160 -0
- package/docs/memory.md +156 -0
- package/docs/models.md +118 -0
- package/docs/reviews.md +231 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +79 -0
- package/package.json +27 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +365 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +671 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runs.py +121 -0
- package/scripts/forge-runtime.py +586 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +240 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +84 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +347 -0
- package/ui/THIRD_PARTY_NOTICES.md +177 -0
- package/ui/docs/api.md +970 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +214 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +582 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1424 -0
- package/ui/forge_ui/server.py +312 -0
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +1079 -0
- package/ui/forge_ui/static/cloud3d.js +647 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +397 -0
- package/ui/forge_ui/static/convo.js +295 -0
- package/ui/forge_ui/static/datatable.js +232 -0
- package/ui/forge_ui/static/diffview.js +235 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/findings3d.js +134 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/icons.js +94 -0
- package/ui/forge_ui/static/index.html +67 -0
- package/ui/forge_ui/static/iso.js +816 -0
- package/ui/forge_ui/static/leaderboard.js +311 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +562 -0
- package/ui/forge_ui/static/runmodel.js +667 -0
- package/ui/forge_ui/static/scene3d.js +1471 -0
- package/ui/forge_ui/static/search.js +120 -0
- package/ui/forge_ui/static/shell.js +137 -0
- package/ui/forge_ui/static/stagectl.js +150 -0
- package/ui/forge_ui/static/style.css +1073 -0
- package/ui/forge_ui/static/theme.js +25 -0
- package/ui/forge_ui/transcripts.py +608 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Per-run selection and frozen routing through the existing dispatcher."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import DEFAULTS, SCRIPTS, command, managed_run, read_json, state_root, write_json
|
|
11
|
+
from .install import doctor, install
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def resolve(spec: str, role: str, yolo: bool = False, dry: bool = False) -> dict:
|
|
15
|
+
import tempfile
|
|
16
|
+
with tempfile.TemporaryDirectory(prefix='forge-fractal-resolve-') as tmp:
|
|
17
|
+
root = Path(tmp)
|
|
18
|
+
prompt = root / 'input'
|
|
19
|
+
prompt.write_text('Offline resolution')
|
|
20
|
+
argv = ['/bin/bash', SCRIPTS / 'forge-dispatch.sh']
|
|
21
|
+
if dry:
|
|
22
|
+
argv += [role, spec, '--run-dir', root, '--prompt-file', prompt, '--dry-run']
|
|
23
|
+
else:
|
|
24
|
+
argv += ['doctor', '--spec', spec, '--role', role]
|
|
25
|
+
if yolo:
|
|
26
|
+
argv += ['--yolo']
|
|
27
|
+
output = command(argv, timeout=60).decode()
|
|
28
|
+
# Dry resolution prints canonical routing without invoking a provider.
|
|
29
|
+
fields = dict(line.split('=', 1) for line in output.splitlines() if '=' in line)
|
|
30
|
+
canonical = ':'.join(fields.get(k, '') for k in ('model', 'effort', 'harness'))
|
|
31
|
+
return dict(spec=spec, role=role, resolution=output, canonical=canonical)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pools(args) -> dict:
|
|
35
|
+
result = {}
|
|
36
|
+
inherited = Path(args.run_dir) / 'fractal-routing.json'
|
|
37
|
+
if inherited.exists():
|
|
38
|
+
result.update(read_json(inherited))
|
|
39
|
+
for tier in ('low', 'medium', 'high', 'any'):
|
|
40
|
+
value = getattr(args, 'dwarf_' + tier, '')
|
|
41
|
+
if value:
|
|
42
|
+
result[tier] = [s.strip() for s in value.split(',') if s.strip()]
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ensure_runtime(allow_ordinary: bool = True) -> bool:
|
|
47
|
+
status = doctor()
|
|
48
|
+
if status['ready']:
|
|
49
|
+
return True
|
|
50
|
+
print(json.dumps(status, indent=2), file=sys.stderr)
|
|
51
|
+
if sys.stdin.isatty():
|
|
52
|
+
from argparse import Namespace
|
|
53
|
+
print('Install Fractal and missing prerequisites? [y/N] ', end='', file=sys.stderr, flush=True)
|
|
54
|
+
if input().lower() == 'y':
|
|
55
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
56
|
+
install(Namespace(yes=True, with_prerequisites=True, dry_run=False))
|
|
57
|
+
elif allow_ordinary:
|
|
58
|
+
print('Use ordinary execution instead? [y/N] ', end='', file=sys.stderr, flush=True)
|
|
59
|
+
if input().lower() == 'y':
|
|
60
|
+
return False
|
|
61
|
+
if not doctor()['ready']:
|
|
62
|
+
raise ValueError('Fractal selected but unavailable. Run forge fractal install --yes --with-prerequisites, then retry')
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def decomposition_settings(args, roots: set[str], previous: dict | None = None) -> dict:
|
|
67
|
+
"""Freeze planner-role resolutions independently of the dwarf routing pools."""
|
|
68
|
+
enabled, explicit = args.fractal_auto_decompose, args.fractal_planner
|
|
69
|
+
if previous is not None:
|
|
70
|
+
saved = previous.get('decomposition', {})
|
|
71
|
+
if enabled and not saved.get('enabled'):
|
|
72
|
+
raise ValueError('Automatic decomposition is frozen; create a new run directory')
|
|
73
|
+
if explicit and (not saved.get('enabled') or explicit != saved.get('planner')):
|
|
74
|
+
raise ValueError('Fractal planner is frozen; create a new run directory')
|
|
75
|
+
return saved
|
|
76
|
+
if explicit and not enabled:
|
|
77
|
+
raise ValueError('--fractal-planner requires --fractal-auto-decompose')
|
|
78
|
+
if not enabled:
|
|
79
|
+
return {}
|
|
80
|
+
inherited = Path(args.run_dir) / 'planner'
|
|
81
|
+
planner = explicit or (inherited.read_text().strip() if inherited.is_file() else '')
|
|
82
|
+
specs = {planner} if planner else roots
|
|
83
|
+
if not specs:
|
|
84
|
+
raise ValueError('Automatic decomposition requires a task root or explicit planner model')
|
|
85
|
+
return dict(enabled=True, version=1, planner=planner,
|
|
86
|
+
resolved=[resolve(s, 'planner', dry=args.dry_run) for s in sorted(specs)])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def decomposition_choice(args) -> str:
|
|
90
|
+
if not args.fractal_auto_decompose:
|
|
91
|
+
return args.choice
|
|
92
|
+
if args.choice == 'off':
|
|
93
|
+
raise ValueError('--fractal-auto-decompose and --no-fractal are mutually exclusive')
|
|
94
|
+
return 'on'
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def select(args) -> str:
|
|
98
|
+
if args.max_cost is not None:
|
|
99
|
+
raise ValueError('Unsupported capability: dollar caps cannot be enforced by the Forge bridge')
|
|
100
|
+
source = Path(args.run_dir).resolve()
|
|
101
|
+
remembered = source / 'fractal-selection.json'
|
|
102
|
+
previous = read_json(remembered) if remembered.exists() else None
|
|
103
|
+
choice = decomposition_choice(args)
|
|
104
|
+
if previous:
|
|
105
|
+
if choice and (choice == 'on') != previous['enabled']:
|
|
106
|
+
raise ValueError('This run already selected a backend; create a new run directory to change it')
|
|
107
|
+
choice = 'on' if previous['enabled'] else 'off'
|
|
108
|
+
if not choice:
|
|
109
|
+
choice = 'off'
|
|
110
|
+
if sys.stdin.isatty() and not args.dry_run:
|
|
111
|
+
print('Use Fractal for this run? [y/N] ', end='', file=sys.stderr, flush=True)
|
|
112
|
+
choice = 'on' if input().lower() == 'y' else 'off'
|
|
113
|
+
if choice == 'off':
|
|
114
|
+
decomposition_settings(args, set())
|
|
115
|
+
if not args.dry_run:
|
|
116
|
+
write_json(remembered, dict(enabled=False))
|
|
117
|
+
return 'off'
|
|
118
|
+
limits = {key: getattr(args, key, None) if getattr(args, key, None) is not None else value
|
|
119
|
+
for key, value in DEFAULTS.items()}
|
|
120
|
+
if any(type(v) is not int or v < (0 if k == 'depth' else 1) for k, v in limits.items()):
|
|
121
|
+
raise ValueError('Fractal limits must be positive integers (depth may be zero)')
|
|
122
|
+
routing = pools(args)
|
|
123
|
+
if args.mode == 'solo' and args.dwarf:
|
|
124
|
+
routing.setdefault('any', [args.dwarf])
|
|
125
|
+
eligible = set(s for pool in routing.values() for s in pool)
|
|
126
|
+
roots = {args.dwarf} if args.dwarf else set()
|
|
127
|
+
if args.dwarf:
|
|
128
|
+
eligible.add(args.dwarf)
|
|
129
|
+
tasks = source / 'tasks.tsv'
|
|
130
|
+
qa_specs = {args.qa} if args.qa else set()
|
|
131
|
+
if tasks.exists():
|
|
132
|
+
for line in tasks.read_text().splitlines():
|
|
133
|
+
if not line or line.startswith('#'):
|
|
134
|
+
continue
|
|
135
|
+
fields = line.split('\t')
|
|
136
|
+
eligible.add(fields[4])
|
|
137
|
+
roots.add(fields[4])
|
|
138
|
+
qa_specs.add(fields[5])
|
|
139
|
+
run_id = 'run-' + hashlib.sha256(str(source).encode()).hexdigest()[:20]
|
|
140
|
+
if previous:
|
|
141
|
+
run = managed_run(previous['run'])
|
|
142
|
+
config = read_json(run / 'run.json')
|
|
143
|
+
decomposition_settings(args, roots, config)
|
|
144
|
+
if config['repository'] != str(Path(args.repo).resolve()):
|
|
145
|
+
raise ValueError('Run repository changed')
|
|
146
|
+
if args.dwarf and args.dwarf not in config['eligible']:
|
|
147
|
+
raise ValueError('Model is outside this run\'s frozen pool')
|
|
148
|
+
if args.qa and args.qa not in config['qa_specs']:
|
|
149
|
+
raise ValueError('QA model changed for an existing run')
|
|
150
|
+
for key in DEFAULTS:
|
|
151
|
+
if getattr(args, key, None) is not None and limits[key] != config['limits'][key]:
|
|
152
|
+
raise ValueError('Limits are frozen for this run; use a new run directory to change ' + key)
|
|
153
|
+
for tier, pool in routing.items():
|
|
154
|
+
if pool != config['routing'].get(tier):
|
|
155
|
+
raise ValueError('Routing pools are frozen for this run')
|
|
156
|
+
if args.dry_run:
|
|
157
|
+
print(json.dumps(config, indent=2), file=sys.stderr)
|
|
158
|
+
return 'dry-fractal'
|
|
159
|
+
ensure_runtime(allow_ordinary=False)
|
|
160
|
+
return str(run)
|
|
161
|
+
config = dict(id=run_id, backend='fractal', mode=args.mode, repository=str(Path(args.repo).resolve()),
|
|
162
|
+
runner=str(source), routing=routing, limits=limits, yolo_dwarf=args.yolo_dwarf,
|
|
163
|
+
eligible=sorted(eligible), qa_specs=sorted(qa_specs), version='1.2.0',
|
|
164
|
+
acceptance='pending', workspace=str(state_root() / 'runs' / run_id / 'tasks'),
|
|
165
|
+
resolved=[resolve(s, 'dwarf', args.yolo_dwarf, dry=args.dry_run) for s in sorted(eligible)])
|
|
166
|
+
config['decomposition'] = decomposition_settings(args, roots)
|
|
167
|
+
for spec in sorted(qa_specs):
|
|
168
|
+
resolve(spec, 'qa', args.yolo_qa, dry=args.dry_run)
|
|
169
|
+
if args.dry_run:
|
|
170
|
+
print(json.dumps(config, indent=2), file=sys.stderr)
|
|
171
|
+
return 'dry-fractal'
|
|
172
|
+
if not ensure_runtime(allow_ordinary=not args.fractal_auto_decompose):
|
|
173
|
+
write_json(remembered, dict(enabled=False))
|
|
174
|
+
return 'off'
|
|
175
|
+
run = state_root() / 'runs' / run_id
|
|
176
|
+
run.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
177
|
+
write_json(run / 'run.json', config)
|
|
178
|
+
write_json(remembered, dict(enabled=True, run=run_id))
|
|
179
|
+
return str(run)
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Opt-in TypeSafe/Jev adapter. Importing this package never makes a network call."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import importlib.util
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
SCRIPTS = Path(__file__).resolve().parents[1]
|
|
11
|
+
|
|
12
|
+
ENDPOINT = 'https://api.typesafe.ai/v1/systemone'
|
|
13
|
+
MODEL = 'jev-latest'
|
|
14
|
+
CAPABILITIES = ('routing', 'tests', 'gates', 'memory')
|
|
15
|
+
DEFAULT_THRESHOLDS = dict(tests_act=0.70, flake_act=0.90,
|
|
16
|
+
gate_warn=0.60,
|
|
17
|
+
# Measured twice. The first 15 prompts were hand-written and
|
|
18
|
+
# scored with only a task TITLE in state, because prompt.md did
|
|
19
|
+
# not reach the model then -- so 0.30 was fitted to data
|
|
20
|
+
# production never produced.
|
|
21
|
+
#
|
|
22
|
+
# Re-measured with the real state, 3 runs over 8 prompts: ones
|
|
23
|
+
# a reviewer could not judge correctness from ("make the error
|
|
24
|
+
# messages better", "harden the server -- fix what you find")
|
|
25
|
+
# scored 0.06-0.27, and ones stating checkable behaviour scored
|
|
26
|
+
# 0.85-0.96. Eight real task prompts landed 0.85-0.96 too. A
|
|
27
|
+
# 0.58 gap, so this sits in the middle of it: 0.28 clear on
|
|
28
|
+
# both sides, against a measured repeat-noise of 0.07.
|
|
29
|
+
# 0.30 also classified every case correctly, but sat 0.03 above
|
|
30
|
+
# the worst true positive -- correct by luck, not by margin.
|
|
31
|
+
prompt_warn=0.55,
|
|
32
|
+
# The rubric asked whether verifying a task needs "work
|
|
33
|
+
# declared in a different task" while the state contained no
|
|
34
|
+
# other task, so the model could only guess whether one
|
|
35
|
+
# existed. That, not the wording, is why the classes used to
|
|
36
|
+
# overlap. The plan is now in state (routing._siblings) and the
|
|
37
|
+
# rubric points at it by name.
|
|
38
|
+
#
|
|
39
|
+
# Re-measured on the 8 real jevscope tasks, ground truth taken
|
|
40
|
+
# from the import graph and wave position rather than from the
|
|
41
|
+
# titles: `server` (wave 1, imports discover/events/stats and
|
|
42
|
+
# serves page's index.html, none declared) 0.26-0.34; `page`
|
|
43
|
+
# (consumes server's API -- both defects that shipped were
|
|
44
|
+
# page<->events shape mismatches) 0.43-0.53; `readme`
|
|
45
|
+
# (documents the wave-2 CLI, and shipped wrong) 0.60-0.65.
|
|
46
|
+
# Genuinely self-contained tasks scored 0.65-0.96.
|
|
47
|
+
#
|
|
48
|
+
# At the old 0.38 this gate caught NOTHING on a real plan, the
|
|
49
|
+
# same way coupling never fired: every fragment scored above
|
|
50
|
+
# it. 0.55 catches server (0.21 clear) and page (0.02 clear --
|
|
51
|
+
# marginal, counted as recall but not relied on), misses readme
|
|
52
|
+
# entirely, and keeps 0.10 to the nearest true negative, which
|
|
53
|
+
# is the only margin here that beats the 0.07 noise floor.
|
|
54
|
+
# Precision is what an advisory gate must hold: a gate that
|
|
55
|
+
# cries wolf is one people switch off.
|
|
56
|
+
#
|
|
57
|
+
# `readme` is not separable from a real task by this rubric --
|
|
58
|
+
# it sits at 0.65 alongside `cli` at 0.65 -- so silence above
|
|
59
|
+
# 0.55 still means "no evidence", not "verifiable".
|
|
60
|
+
#
|
|
61
|
+
# Equal to prompt_warn by coincidence, not by kinship. They
|
|
62
|
+
# were measured separately against different rubrics and must
|
|
63
|
+
# stay separate keys; collapsing them into one constant is the
|
|
64
|
+
# exact mistake this file has now recorded six times.
|
|
65
|
+
verifiable_warn=0.55,
|
|
66
|
+
# Coupling borrowed gate_warn (0.60) and therefore never fired
|
|
67
|
+
# once, on any plan. Measured over 11 same-wave pairs from a
|
|
68
|
+
# real 8-task plan, 5 runs each, with ground truth taken from
|
|
69
|
+
# the run itself -- two of those pairs produced a defect that
|
|
70
|
+
# actually shipped. Coupling probabilities live at 0.09-0.27.
|
|
71
|
+
# 0.60 was not a strict threshold, it was unreachable.
|
|
72
|
+
#
|
|
73
|
+
# At 0.20: 4 of 5 genuinely coupled pairs warn, 0 of 6
|
|
74
|
+
# independent ones do. Provisional -- one plan, one repo, and
|
|
75
|
+
# margins of about 0.02 either side -- but a gate that fires on
|
|
76
|
+
# the right pairs 4 times in 5 beats one that cannot fire.
|
|
77
|
+
coupling_warn=0.20,
|
|
78
|
+
# The ordering question coupling could not ask: an earlier-wave
|
|
79
|
+
# task written against what a LATER task builds. jevscope's
|
|
80
|
+
# `readme` ran in wave 1 documenting the wave-2 CLI, passed its
|
|
81
|
+
# own review because the thing it was wrong about did not exist
|
|
82
|
+
# yet, and shipped describing an interface nobody wrote.
|
|
83
|
+
#
|
|
84
|
+
# Measured over that plan's 7 cross-wave pairs, 5 runs each:
|
|
85
|
+
# readme->cli 0.88-0.90, every independent pair 0.05-0.15. A
|
|
86
|
+
# 0.73-wide gap, the cleanest any rubric here has produced, so
|
|
87
|
+
# this sits in the middle with 0.35 clear below and 0.38 above
|
|
88
|
+
# against a 0.07 noise floor.
|
|
89
|
+
#
|
|
90
|
+
# Provisional in one specific way: the gap is enormous but it
|
|
91
|
+
# rests on a single positive example. A second plan could move
|
|
92
|
+
# the positive class; it is unlikely to move it 0.38.
|
|
93
|
+
forward_warn=0.50,
|
|
94
|
+
# Two rubrics, and they were measured separately because a
|
|
95
|
+
# shared name is not a shared scale. The measurement then said
|
|
96
|
+
# the scales ARE close -- a correct judgment scores 0.91+ on
|
|
97
|
+
# both, a wrong one 0.72 or less on both -- so what separates
|
|
98
|
+
# these two numbers is not the scale but the cost of being
|
|
99
|
+
# wrong, and each sits at a different point in the same gap.
|
|
100
|
+
#
|
|
101
|
+
# A wrong merge loses a fact permanently, so it takes the top
|
|
102
|
+
# of the gap: 0.13 clear of the worst false merge, 0.06 under
|
|
103
|
+
# the weakest true one.
|
|
104
|
+
memory_merge=0.85,
|
|
105
|
+
# A wrong refile is recoverable -- the fact is still there,
|
|
106
|
+
# under the wrong heading -- and refusing a correction is the
|
|
107
|
+
# commoner harm, so it takes the bottom of the same gap.
|
|
108
|
+
memory_recategorize=0.80,
|
|
109
|
+
# Deliberately far below gate_warn: dropping a fact is
|
|
110
|
+
# irreversible, so only a confident "this went false" acts.
|
|
111
|
+
stale_drop=0.25)
|
|
112
|
+
DEFAULTS = dict(enabled=False, key='', model=MODEL, endpoint=ENDPOINT, deadline_s=5.0,
|
|
113
|
+
capabilities={c: True for c in CAPABILITIES},
|
|
114
|
+
thresholds=dict(DEFAULT_THRESHOLDS),
|
|
115
|
+
# A second provider, per site. Empty means every site asks TypeSafe exactly
|
|
116
|
+
# as before; see "Cloudflare Clef" in references/jev.md.
|
|
117
|
+
providers={}, sites={}, model_thresholds={})
|
|
118
|
+
|
|
119
|
+
# Cloudflare Workers AI's hosted Clef: Jev's request shape, a different envelope and
|
|
120
|
+
# limits. Context is state AND questions together; a text state that does not fit is
|
|
121
|
+
# cut silently by the service, so the client refuses rather than send it.
|
|
122
|
+
PROVIDERS = ('typesafe', 'cloudflare')
|
|
123
|
+
CLOUDFLARE_URL = 'https://api.cloudflare.com/client/v4/accounts/{account}/ai/run/@cf/cloudflare/{model}'
|
|
124
|
+
CLOUDFLARE_MODEL = 'clef'
|
|
125
|
+
CLOUDFLARE_CONTEXT = {'clef': 65536, 'clef-flash': 24576}
|
|
126
|
+
CLOUDFLARE_MAX_QUESTIONS = 64
|
|
127
|
+
# Beside jev.json (honoring XDG_CONFIG_HOME), so a test's private config home never reads
|
|
128
|
+
# the developer's real token.
|
|
129
|
+
CLOUDFLARE_TOKEN_FILE = 'cloudflare.env'
|
|
130
|
+
|
|
131
|
+
# Which capability each site belongs to, so a site on a provider with no credentials is
|
|
132
|
+
# not ready even when its capability is on. clef-* sites exist only on Cloudflare: they
|
|
133
|
+
# ask rubrics TypeSafe was never measured on, and record silently.
|
|
134
|
+
SITE_CAPABILITY = {
|
|
135
|
+
'jev-score-plan': 'routing',
|
|
136
|
+
'clef-first-pass': 'routing', 'clef-retry-pass': 'routing',
|
|
137
|
+
'jev-drift': 'gates', 'jev-coupling': 'gates', 'jev-forward': 'gates',
|
|
138
|
+
'jev-review-triage': 'gates',
|
|
139
|
+
'jev-memory-curate': 'memory', 'jev-memory-stale': 'memory', 'jev-memory-slice': 'memory',
|
|
140
|
+
'jev-test-subset': 'tests', 'jev-verify-discover': 'tests', 'jev-verify-triage': 'tests',
|
|
141
|
+
}
|
|
142
|
+
CLEF_ONLY_SITES = ('clef-first-pass', 'clef-retry-pass')
|
|
143
|
+
|
|
144
|
+
# Loaded via importlib rather than a package-relative import: forge-runtime.py lives one
|
|
145
|
+
# directory up and is not itself part of a package (same trick as forge_fractal).
|
|
146
|
+
_runtime_spec = importlib.util.spec_from_file_location('forge_runtime', SCRIPTS / 'forge-runtime.py')
|
|
147
|
+
_runtime = importlib.util.module_from_spec(_runtime_spec)
|
|
148
|
+
_runtime_spec.loader.exec_module(_runtime)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def config_path() -> Path:
|
|
152
|
+
# An empty XDG_CONFIG_HOME counts as unset (XDG spec): taken literally it put jev.json
|
|
153
|
+
# below the current directory.
|
|
154
|
+
return Path(os.environ.get('XDG_CONFIG_HOME') or str(Path.home() / '.config')).expanduser() / 'forge/jev.json'
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def load_config() -> dict:
|
|
158
|
+
data = dict(DEFAULTS)
|
|
159
|
+
data['capabilities'] = dict(DEFAULTS['capabilities'])
|
|
160
|
+
data['thresholds'] = dict(DEFAULTS['thresholds'])
|
|
161
|
+
try:
|
|
162
|
+
stored = json.loads(config_path().read_text())
|
|
163
|
+
except (OSError, ValueError):
|
|
164
|
+
return data
|
|
165
|
+
# Valid JSON that is not an object (a list, a bare string, null) would make update()
|
|
166
|
+
# raise, and a config file is not worth failing a run over. Treat it as absent.
|
|
167
|
+
if not isinstance(stored, dict):
|
|
168
|
+
return data
|
|
169
|
+
data.update(stored)
|
|
170
|
+
for section in ('capabilities', 'thresholds', 'providers', 'sites', 'model_thresholds'):
|
|
171
|
+
value = stored.get(section)
|
|
172
|
+
data[section] = dict(DEFAULTS[section], **value) if isinstance(value, dict) else dict(DEFAULTS[section])
|
|
173
|
+
return data
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def save_config(data: dict) -> None:
|
|
177
|
+
path = config_path()
|
|
178
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
temporary = path.with_name(path.name + '.' + uuid.uuid4().hex)
|
|
180
|
+
temporary.write_text(json.dumps(data, indent=2) + '\n')
|
|
181
|
+
temporary.chmod(0o600)
|
|
182
|
+
temporary.replace(path)
|
|
183
|
+
path.chmod(0o600)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def api_key(config: dict | None = None) -> str | None:
|
|
187
|
+
env = os.environ.get('TYPESAFE_API_KEY')
|
|
188
|
+
if env:
|
|
189
|
+
return env
|
|
190
|
+
key = (config if config is not None else load_config()).get('key')
|
|
191
|
+
return key or None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def redact(text: str, *keys: str) -> str:
|
|
195
|
+
for key in keys:
|
|
196
|
+
if key:
|
|
197
|
+
text = text.replace(key, '***')
|
|
198
|
+
for var in ('TYPESAFE_API_KEY', 'CLOUDFLARE_API_TOKEN'):
|
|
199
|
+
env = os.environ.get(var)
|
|
200
|
+
if env:
|
|
201
|
+
text = text.replace(env, '***')
|
|
202
|
+
return text
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _env_file(path: str) -> dict:
|
|
206
|
+
"""KEY=VALUE lines of a private env file; {} when it is missing or unreadable."""
|
|
207
|
+
out = {}
|
|
208
|
+
try:
|
|
209
|
+
lines = Path(path).expanduser().read_text().splitlines()
|
|
210
|
+
except (OSError, UnicodeDecodeError):
|
|
211
|
+
return out
|
|
212
|
+
for line in lines:
|
|
213
|
+
line = line.strip()
|
|
214
|
+
if not line or line.startswith('#') or '=' not in line:
|
|
215
|
+
continue
|
|
216
|
+
name, _, value = line.partition('=')
|
|
217
|
+
name = name.strip()
|
|
218
|
+
if name.startswith('export '):
|
|
219
|
+
name = name[len('export '):].strip()
|
|
220
|
+
out[name] = value.strip().strip('"').strip("'")
|
|
221
|
+
return out
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cloudflare(config: dict | None = None) -> dict | None:
|
|
225
|
+
"""The Cloudflare provider's account, token and model, or None when not configured.
|
|
226
|
+
|
|
227
|
+
Configured means a `providers.cloudflare` entry exists, or the token is in the
|
|
228
|
+
environment. The token comes from CLOUDFLARE_API_TOKEN, else the entry's `token`,
|
|
229
|
+
else its `token_file` (default cloudflare.env beside jev.json) -- never from argv.
|
|
230
|
+
"""
|
|
231
|
+
config = config if config is not None else load_config()
|
|
232
|
+
entry = (config.get('providers') or {}).get('cloudflare')
|
|
233
|
+
if not isinstance(entry, dict):
|
|
234
|
+
entry = None
|
|
235
|
+
if entry is None and not os.environ.get('CLOUDFLARE_API_TOKEN'):
|
|
236
|
+
return None
|
|
237
|
+
entry = entry or {}
|
|
238
|
+
from_file = _env_file(entry.get('token_file') or str(config_path().parent / CLOUDFLARE_TOKEN_FILE))
|
|
239
|
+
token = os.environ.get('CLOUDFLARE_API_TOKEN') or entry.get('token') or from_file.get('CLOUDFLARE_API_TOKEN')
|
|
240
|
+
account = (os.environ.get('CLOUDFLARE_ACCOUNT_ID') or entry.get('account_id')
|
|
241
|
+
or from_file.get('CLOUDFLARE_ACCOUNT_ID'))
|
|
242
|
+
model = entry.get('model') or CLOUDFLARE_MODEL
|
|
243
|
+
# `url` is a template with {account} and {model}: an AI Gateway route, or a local test server.
|
|
244
|
+
return dict(token=token or None, account_id=account or None,
|
|
245
|
+
model=model if model in CLOUDFLARE_CONTEXT else CLOUDFLARE_MODEL,
|
|
246
|
+
url=entry.get('url') or CLOUDFLARE_URL)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def secrets(config: dict | None = None) -> list:
|
|
250
|
+
"""Every credential forge might send, for redaction."""
|
|
251
|
+
config = config if config is not None else load_config()
|
|
252
|
+
out = [api_key(config)]
|
|
253
|
+
cf = cloudflare(config)
|
|
254
|
+
if cf:
|
|
255
|
+
out.append(cf['token'])
|
|
256
|
+
return [s for s in out if s]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def provider_for(site: str, config: dict | None = None) -> str:
|
|
260
|
+
"""Which provider answers `site`: the site map, unless an env override says otherwise."""
|
|
261
|
+
if site in CLEF_ONLY_SITES:
|
|
262
|
+
return 'cloudflare'
|
|
263
|
+
if os.environ.get('FORGE_CLEF') == 'off':
|
|
264
|
+
return 'typesafe'
|
|
265
|
+
forced = os.environ.get('FORGE_JEV_PROVIDER')
|
|
266
|
+
if forced in PROVIDERS:
|
|
267
|
+
return forced
|
|
268
|
+
config = config if config is not None else load_config()
|
|
269
|
+
chosen = (config.get('sites') or {}).get(site)
|
|
270
|
+
return chosen if chosen in PROVIDERS else 'typesafe'
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def provider_ready(name: str, config: dict | None = None) -> bool:
|
|
274
|
+
config = config if config is not None else load_config()
|
|
275
|
+
if name == 'cloudflare':
|
|
276
|
+
cf = cloudflare(config)
|
|
277
|
+
return bool(cf and cf['token'] and cf['account_id'])
|
|
278
|
+
return api_key(config) is not None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def site_ready(site: str, config: dict | None = None) -> bool:
|
|
282
|
+
"""The site's capability is on and its provider has credentials. Offline: reads config only."""
|
|
283
|
+
config = config if config is not None else load_config()
|
|
284
|
+
capability = SITE_CAPABILITY.get(site)
|
|
285
|
+
if not enabled(capability, config=config):
|
|
286
|
+
return False
|
|
287
|
+
if site in CLEF_ONLY_SITES:
|
|
288
|
+
entry = (config.get('providers') or {}).get('cloudflare')
|
|
289
|
+
if (os.environ.get('FORGE_CLEF') == 'off' or os.environ.get('FORGE_JEV_OUTCOMES') == 'off'
|
|
290
|
+
or (isinstance(entry, dict) and entry.get('outcomes') is False)):
|
|
291
|
+
return False
|
|
292
|
+
return provider_ready(provider_for(site, config), config)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def capability_ready(capability: str, config: dict | None = None) -> bool:
|
|
296
|
+
"""Some site of this capability can be asked: what the shell's forge_jev_active tests."""
|
|
297
|
+
config = config if config is not None else load_config()
|
|
298
|
+
return any(site_ready(site, config) for site, cap in SITE_CAPABILITY.items()
|
|
299
|
+
if cap == capability and site not in CLEF_ONLY_SITES)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _capability_overrides() -> dict[str, str]:
|
|
303
|
+
names = dict(routing='FORGE_JEV_ROUTING', tests='FORGE_JEV_TESTS', gates='FORGE_JEV_GATES', memory='FORGE_JEV_MEMORY')
|
|
304
|
+
return {cap: os.environ[var] for cap, var in names.items() if os.environ.get(var) in ('on', 'off')}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def enabled(capability: str | None = None, *, config: dict | None = None) -> bool:
|
|
308
|
+
if os.environ.get('FORGE_JEV') == 'off':
|
|
309
|
+
return False
|
|
310
|
+
overrides = _capability_overrides()
|
|
311
|
+
# An explicit --jev-<cap> is an allowlist, so naming one capability does not
|
|
312
|
+
# silently enable the rest.
|
|
313
|
+
if 'on' in overrides.values():
|
|
314
|
+
return capability is None or overrides.get(capability) == 'on'
|
|
315
|
+
if overrides.get(capability) == 'off':
|
|
316
|
+
return False
|
|
317
|
+
stored = config if config is not None else load_config()
|
|
318
|
+
if os.environ.get('FORGE_JEV') != 'on' and not stored['enabled']:
|
|
319
|
+
return False
|
|
320
|
+
return capability is None or stored['capabilities'].get(capability, True)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def acting() -> bool:
|
|
324
|
+
return os.environ.get('FORGE_JEV_ACT') == 'on'
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def shadow() -> bool:
|
|
328
|
+
return os.environ.get('FORGE_JEV_SHADOW') == 'on'
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def threshold(name: str, *, config: dict | None = None, model: str | None = None) -> float:
|
|
332
|
+
"""A rubric's cutoff. `model` is the model that answered: a threshold measured on one
|
|
333
|
+
model's probabilities means nothing on another's, so a per-model value wins."""
|
|
334
|
+
config = config or load_config()
|
|
335
|
+
if model:
|
|
336
|
+
per_model = dict(DEFAULT_MODEL_THRESHOLDS.get(model, {}),
|
|
337
|
+
**((config.get('model_thresholds') or {}).get(model) or {}))
|
|
338
|
+
if name in per_model:
|
|
339
|
+
return per_model[name]
|
|
340
|
+
if name not in config['thresholds'] and name in THRESHOLD_FALLBACK:
|
|
341
|
+
return threshold(THRESHOLD_FALLBACK[name], config=config)
|
|
342
|
+
return config['thresholds'].get(name, DEFAULT_THRESHOLDS.get(name))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# Rubric thresholds introduced after a shared one: until measured separately they read the
|
|
346
|
+
# old shared value, so a config that never named them behaves exactly as before.
|
|
347
|
+
THRESHOLD_FALLBACK = dict(drift_warn='gate_warn')
|
|
348
|
+
# Thresholds whose only reader was removed on 2026-10-17 (routing's --jev-act tier rewrite
|
|
349
|
+
# and `forge jev calibrate`). A config written before then still holds them: they are
|
|
350
|
+
# ignored, and `doctor` names them as safe to delete rather than as unknown.
|
|
351
|
+
RETIRED_THRESHOLDS = frozenset(('routing_act', 'calibrate_floor'))
|
|
352
|
+
# Per-model cutoffs, only where a replay justified one. Clef has none yet:
|
|
353
|
+
#
|
|
354
|
+
# Replayed 2026-10-16 on the 38 live jev-drift requests whose task outcome could be joined
|
|
355
|
+
# (4560 decisions, 16 positives, 17 runs; older half of runs to calibrate, newer half to
|
|
356
|
+
# evaluate). Clef ranks drift at least as well as Jev on the held-out runs (AUC 0.985 vs
|
|
357
|
+
# 0.972, paired run-bootstrap difference [0.000, +0.028]) and at the shared 0.60 cutoff its
|
|
358
|
+
# warnings are no less precise (1 of 1 against 1 of 2). The calibration half could not fit
|
|
359
|
+
# a cutoff of its own: neither model warned precisely there (Jev flagged nothing at 0.60).
|
|
360
|
+
# So drift_warn keeps reading gate_warn for Clef too; the evaluation half hints 0.40 (3 of
|
|
361
|
+
# 4 right), but choosing it there would be fitting on the data that judges it.
|
|
362
|
+
DEFAULT_MODEL_THRESHOLDS: dict = {}
|
|
363
|
+
|
|
364
|
+
# The sites a replay showed Clef non-inferior on, which `forge jev setup --provider
|
|
365
|
+
# cloudflare` maps by default. jev-memory-slice tied (AUC 0.951 vs 0.953) on too few held-out
|
|
366
|
+
# runs (5) to clear the rule; jev-review-triage has no live label for its correctness half.
|
|
367
|
+
CLOUDFLARE_DEFAULT_SITES = ('jev-drift',)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def write_json(path: Path, value: object) -> None:
|
|
371
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
372
|
+
_runtime.write_json(path, value)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def read_json(path: Path) -> dict:
|
|
376
|
+
return json.loads(path.read_text())
|