arkaos 5.17.2 → 5.18.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/THE-ARKAOS-GUIDE.md +2 -2
- package/VERSION +1 -1
- package/arka/SKILL.md +2 -1
- package/arka/skills/costs/SKILL.md +16 -0
- package/config/claude-agents/marta-cqo.md +10 -0
- package/config/decisions/corpora/bash-effect.jsonl +38 -0
- package/config/decisions/corpora/creation-intent.jsonl +47 -0
- package/config/decisions/corpora/dispatch-role.jsonl +32 -0
- package/config/decisions/corpora/forge-complexity.jsonl +102 -0
- package/config/decisions/corpora/forge-departments.jsonl +34 -0
- package/config/decisions/corpora/heldout/learning-signal.jsonl +17 -0
- package/config/decisions/corpora/heldout/qg-prescreen.jsonl +20 -0
- package/config/decisions/corpora/heldout/slop-score.jsonl +20 -0
- package/config/decisions/corpora/learning-signal.jsonl +35 -0
- package/config/decisions/corpora/phantom-action.jsonl +34 -0
- package/config/decisions/corpora/qg-prescreen.jsonl +46 -0
- package/config/decisions/corpora/refine.jsonl +104 -0
- package/config/decisions/corpora/route.jsonl +33 -0
- package/config/decisions/corpora/skill-hints.jsonl +34 -0
- package/config/decisions/corpora/skill-proposer.jsonl +32 -0
- package/config/decisions/corpora/slop-score.jsonl +33 -0
- package/config/decisions/corpora/subagent-discipline.jsonl +32 -0
- package/config/decisions/corpora/sycophancy.jsonl +34 -0
- package/config/decisions/corpora/topic-drift.jsonl +34 -0
- package/config/decisions/corpora/ui-in-ts.jsonl +60 -0
- package/config/hooks/gate-manifest.json +1 -0
- package/config/models.yaml +10 -2
- package/core/decisions/__init__.py +8 -0
- package/core/decisions/backoff.py +137 -0
- package/core/decisions/cache.py +92 -0
- package/core/decisions/client.py +191 -0
- package/core/decisions/config.py +159 -0
- package/core/decisions/cost_effect.py +242 -0
- package/core/decisions/engine.py +400 -0
- package/core/decisions/models.py +118 -0
- package/core/decisions/paths.py +39 -0
- package/core/decisions/privacy.py +378 -0
- package/core/decisions/promotion.py +126 -0
- package/core/decisions/registry.py +23 -0
- package/core/decisions/replay.py +852 -0
- package/core/decisions/replay_report.py +413 -0
- package/core/decisions/shadow.py +147 -0
- package/core/decisions/site.py +343 -0
- package/core/decisions/sites/__init__.py +1 -0
- package/core/decisions/sites/command.py +58 -0
- package/core/decisions/sites/dispatch.py +161 -0
- package/core/decisions/sites/forge.py +230 -0
- package/core/decisions/sites/governance.py +361 -0
- package/core/decisions/sites/prompt.py +195 -0
- package/core/decisions/sites/quality.py +281 -0
- package/core/decisions/telemetry.py +335 -0
- package/core/decisions/telemetry_cli.py +149 -0
- package/core/decisions/transport.py +97 -0
- package/core/egress/audit.py +46 -6
- package/core/egress/credentials.py +1296 -0
- package/core/egress/policy.py +7 -4
- package/core/evals/verdict_labels.py +25 -5
- package/core/forge/budget.py +32 -0
- package/core/forge/complexity.py +11 -3
- package/core/forge/jev_sites.py +80 -0
- package/core/forge/orchestrator.py +76 -72
- package/core/governance/aggregate_guard.py +175 -77
- package/core/governance/carry_advisor.py +36 -19
- package/core/governance/evidence_checks.py +134 -55
- package/core/governance/fence_recovery.py +165 -0
- package/core/governance/harness_scanner.py +4 -0
- package/core/governance/jev_advisory.py +154 -0
- package/core/governance/leak_scanner.py +2 -2
- package/core/governance/literal_git.py +66 -0
- package/core/governance/qg_prescreen.py +518 -0
- package/core/governance/qg_tier.py +17 -0
- package/core/governance/reviewer_ledger.py +225 -46
- package/core/governance/skill_proposer.py +38 -13
- package/core/governance/slop_check.py +169 -0
- package/core/hooks/operator_message.py +128 -0
- package/core/hooks/stop.py +418 -38
- package/core/hooks/stop_budget.py +43 -0
- package/core/hooks/subagent_stop.py +39 -9
- package/core/hooks/ups_dispatch.py +243 -0
- package/core/hooks/user_prompt_submit.py +458 -106
- package/core/keys.py +4 -0
- package/core/runtime/model_router.py +38 -0
- package/core/runtime/pricing.py +8 -0
- package/core/synapse/command_menu.py +70 -0
- package/core/synapse/layers.py +108 -66
- package/core/workflow/flow_enforcer.py +71 -1
- package/core/workflow/frontend_gate.py +57 -1
- package/core/workflow/transcript_scope.py +34 -25
- package/departments/quality/SKILL.md +29 -2
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/zed/.rules +1 -1
- package/installer/config-seed.js +71 -1
- package/installer/keys.js +1 -1
- package/installer/update.js +29 -0
- package/knowledge/commands-registry.json +17 -4
- package/knowledge/commands-registry.json.bak +6739 -0
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/synapse-bridge.py +17 -1
- package/scripts/tools/corpus_merge.py +402 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
"""Replay harness — the evidence gate for moving a site between modes.
|
|
2
|
+
|
|
3
|
+
Runs a labelled corpus (``config/decisions/corpora/<site>.jsonl``) through
|
|
4
|
+
the site's heuristic and, online, through Jev. Gate (online):
|
|
5
|
+
|
|
6
|
+
* Jev precision on the cases it answers ≥ heuristic precision on the SAME
|
|
7
|
+
cases + 5 pp;
|
|
8
|
+
* abstain rate (abstain + unavailable) ≤ 25 %;
|
|
9
|
+
* escalate-only sites: false escalations ≤ 5 % of the cases where the
|
|
10
|
+
heuristic was already right.
|
|
11
|
+
|
|
12
|
+
Corpus validity (both modes): ≥ 30 cases, ≥ 50 % pt-PT, and every
|
|
13
|
+
``expected`` of the site's type (:data:`EXPECTED`: bool, a department, a
|
|
14
|
+
role, a tier, a department list, a registry command id, a learning signal,
|
|
15
|
+
a ``[verdict, blocker]`` pair or five 1-10 slop scores). ``--offline``
|
|
16
|
+
scores the heuristic only and passes on a valid corpus.
|
|
17
|
+
|
|
18
|
+
Sites whose value is not the corpus label compare through :data:`LABELS`
|
|
19
|
+
(forge-complexity: dimensions → tier; forge-departments: order-free set;
|
|
20
|
+
learning-signal: the ``signal``; qg-prescreen: ``[verdict, blocker]``;
|
|
21
|
+
slop-score: the five scores in rubric order).
|
|
22
|
+
|
|
23
|
+
Two PR3 sites have no heuristic, so "Jev vs heuristic" is not their gate:
|
|
24
|
+
|
|
25
|
+
* ``qg-prescreen`` replays against the neutral ``{"verdict": "unknown"}``
|
|
26
|
+
baseline, which never matches a label: the report measures Jev against
|
|
27
|
+
the real label only (the precision rule reduces to Jev ≥ 5 %, the
|
|
28
|
+
abstain rule is what binds).
|
|
29
|
+
* ``slop-score`` (:data:`MAE_SITES`): the precision rule is replaced by
|
|
30
|
+
the mean absolute error of Jev's TOTAL (5-50) against the labelled total
|
|
31
|
+
on the answered cases, ``mean_abs_error`` ≤ :data:`MAX_SLOP_MAE`; exact
|
|
32
|
+
five-score agreement is still reported as ``jev_accuracy``.
|
|
33
|
+
|
|
34
|
+
Cases may carry ``context``: the site inputs the live call site has
|
|
35
|
+
besides the text (phantom-action ``tool_uses``, ui-in-ts ``path``).
|
|
36
|
+
Exit codes: 0 pass, 1 fail, 2 usage.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import argparse
|
|
42
|
+
import json
|
|
43
|
+
import re
|
|
44
|
+
import statistics
|
|
45
|
+
import sys
|
|
46
|
+
from collections.abc import Callable, Sequence
|
|
47
|
+
from dataclasses import asdict, dataclass, field, replace
|
|
48
|
+
from functools import lru_cache
|
|
49
|
+
from pathlib import Path
|
|
50
|
+
from typing import Literal
|
|
51
|
+
|
|
52
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, ValidationError
|
|
53
|
+
|
|
54
|
+
from core.decisions.client import LOCAL_MARK
|
|
55
|
+
from core.decisions.config import (
|
|
56
|
+
DecisionsConfig,
|
|
57
|
+
load_decisions_config,
|
|
58
|
+
site_timeout_ms,
|
|
59
|
+
threshold_for,
|
|
60
|
+
)
|
|
61
|
+
from core.decisions.engine import resolve, run_sync
|
|
62
|
+
from core.decisions.models import Answer, State
|
|
63
|
+
from core.decisions.paths import repo_root
|
|
64
|
+
from core.decisions.registry import SITES
|
|
65
|
+
from core.decisions.site import Outcome, Site, SiteCall
|
|
66
|
+
from core.decisions.sites import governance as gov
|
|
67
|
+
from core.decisions.sites import quality
|
|
68
|
+
from core.decisions.sites.command import command_state
|
|
69
|
+
from core.decisions.sites.dispatch import skill_state
|
|
70
|
+
from core.decisions.sites.forge import forge_state, single_cell_complexity
|
|
71
|
+
from core.decisions.sites.prompt import prompt_state
|
|
72
|
+
from core.decisions.telemetry import REPLAY_COST_CATEGORY, REPLAY_SESSION_ID, call_cost_usd
|
|
73
|
+
from core.decisions.transport import Transport, resolve_transport
|
|
74
|
+
|
|
75
|
+
# dispatch-role and subagent-discipline: the keyword baselines the UPS hook
|
|
76
|
+
# ships as those sites' heuristics — replayed as the SAME functions.
|
|
77
|
+
from core.hooks.ups_dispatch import keyword_dispatch_role, keyword_needs_isolation
|
|
78
|
+
|
|
79
|
+
MIN_CASES = 30
|
|
80
|
+
MIN_PT_SHARE = 0.5
|
|
81
|
+
MARGIN = 0.05
|
|
82
|
+
MAX_ABSTAIN = 0.25
|
|
83
|
+
MAX_FALSE_ESCALATION = 0.05
|
|
84
|
+
MAX_SLOP_MAE = 5.0 # of a 5-50 total: one point per dimension on average
|
|
85
|
+
MAE_SITES = frozenset({"slop-score"})
|
|
86
|
+
# Held-out sets (spec PR5 D7): small generalisation checks, never mixed
|
|
87
|
+
# into the corpora above; a held-out run is reported on its own.
|
|
88
|
+
HELDOUT_SITES = ("learning-signal", "qg-prescreen", "slop-score")
|
|
89
|
+
MIN_HELDOUT_CASES = 15
|
|
90
|
+
|
|
91
|
+
Gate = Literal["pass", "fail", "offline"]
|
|
92
|
+
CorpusKind = Literal["corpus", "heldout"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ``seed`` or a labeller batch ``handwritten-<batch>``; anything else is a typo.
|
|
96
|
+
SOURCE_PATTERN = r"^(seed|handwritten-[A-Za-z0-9][A-Za-z0-9-]*)$"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ReplayCase(BaseModel):
|
|
100
|
+
"""One labelled prompt.
|
|
101
|
+
|
|
102
|
+
``source`` says where the case came from and is validated against
|
|
103
|
+
:data:`SOURCE_PATTERN`: ``seed`` for the PR1-PR3 corpora, or
|
|
104
|
+
``handwritten-<batch>`` for a labeller's relabel batch (e.g.
|
|
105
|
+
``handwritten-2026-09-25-A``); the adjudication itself lives in the
|
|
106
|
+
spec, never in the jsonl. ``gap`` is refine's missing piece
|
|
107
|
+
(:data:`~core.decisions.sites.prompt.REFINE_GAPS`), compared with
|
|
108
|
+
Jev's ``missing`` choice as a secondary score; the vague bool in
|
|
109
|
+
``expected`` stays the gate's label.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(extra="forbid")
|
|
113
|
+
|
|
114
|
+
id: str
|
|
115
|
+
lang: Literal["pt", "en"]
|
|
116
|
+
prompt: str
|
|
117
|
+
prior: list[str] = []
|
|
118
|
+
context: dict[str, StrictInt | str] = {}
|
|
119
|
+
expected: StrictBool | str | list[str] | list[StrictInt]
|
|
120
|
+
source: str = Field("seed", pattern=SOURCE_PATTERN)
|
|
121
|
+
gap: str | None = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class ReplayReport:
|
|
126
|
+
"""Scores and gate verdict for one site's corpus."""
|
|
127
|
+
|
|
128
|
+
site: str
|
|
129
|
+
cases: int
|
|
130
|
+
pt_share: float
|
|
131
|
+
heuristic_accuracy: float
|
|
132
|
+
jev_accuracy: float | None = None
|
|
133
|
+
heuristic_on_answered: float | None = None
|
|
134
|
+
act_accuracy: float | None = None
|
|
135
|
+
abstain_rate: float | None = None
|
|
136
|
+
false_escalation_rate: float | None = None
|
|
137
|
+
# MAE_SITES only: mean |Jev total - labelled total| on the answered cases.
|
|
138
|
+
mean_abs_error: float | None = None
|
|
139
|
+
unavailable: int = 0
|
|
140
|
+
# G3 evidence: median latency of the calls that really went out
|
|
141
|
+
# (cache hits and an open breaker excluded) and their summed cost.
|
|
142
|
+
# None whenever nothing was measured (offline, or every answer from
|
|
143
|
+
# the cache), so "not measured" never reads as "0 ms measured".
|
|
144
|
+
p50_latency_ms: int | None = None
|
|
145
|
+
cost_usd: float | None = None
|
|
146
|
+
# The per-call ceiling of an online run: the site's own ceiling (what
|
|
147
|
+
# the live call site uses) unless ``--timeout-ms`` overrides it.
|
|
148
|
+
timeout_ms: int | None = None
|
|
149
|
+
# Online only: cases whose site asked a question (a designed
|
|
150
|
+
# "no-questions" is not asked) and, of those, calls that reached the
|
|
151
|
+
# endpoint. ``replay_report`` counts a run only when enough reached.
|
|
152
|
+
asked: int | None = None
|
|
153
|
+
reached: int | None = None
|
|
154
|
+
by_lang: dict[str, dict[str, float | None]] = field(default_factory=dict)
|
|
155
|
+
# refine only: Jev's ``missing`` choice vs the labelled ``gap`` on the
|
|
156
|
+
# cases where both exist (secondary: never part of the gate).
|
|
157
|
+
gap_accuracy: float | None = None
|
|
158
|
+
# forge-complexity only (D6): the abstain rate the pre-D6 single-cell
|
|
159
|
+
# reading gives on the SAME answers, reported next to the window one.
|
|
160
|
+
single_cell_abstain_rate: float | None = None
|
|
161
|
+
corpus: CorpusKind = "corpus"
|
|
162
|
+
gate: Gate = "offline"
|
|
163
|
+
failures: list[str] = field(default_factory=list)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --- heuristics: the SAME pure functions the live call sites run ------------
|
|
167
|
+
|
|
168
|
+
def _heuristic_topic_drift(case: ReplayCase) -> bool:
|
|
169
|
+
from core.hooks.user_prompt_submit import keyword_topic_shift
|
|
170
|
+
|
|
171
|
+
return keyword_topic_shift(case.prompt, "\n".join(case.prior))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _heuristic_refine(case: ReplayCase) -> bool:
|
|
175
|
+
from core.hooks.user_prompt_submit import (
|
|
176
|
+
_names_concrete_target,
|
|
177
|
+
_refine_heuristic,
|
|
178
|
+
_wf_classify,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
text = case.prompt
|
|
182
|
+
if not _wf_classify(text) or text.strip().startswith("/"):
|
|
183
|
+
return False
|
|
184
|
+
return not _names_concrete_target(text) and _refine_heuristic(text)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _heuristic_creation(case: ReplayCase) -> bool:
|
|
188
|
+
from core.hooks.user_prompt_submit import _wf_classify
|
|
189
|
+
|
|
190
|
+
return _wf_classify(case.prompt)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _heuristic_route(case: ReplayCase) -> str:
|
|
194
|
+
from core.synapse.layers import keyword_department
|
|
195
|
+
|
|
196
|
+
return keyword_department(case.prompt) or ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _heuristic_bash_effect(case: ReplayCase) -> bool:
|
|
200
|
+
from core.workflow.flow_enforcer import bash_is_effect
|
|
201
|
+
|
|
202
|
+
return bash_is_effect(case.prompt)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def forge_estimates(prompt: str) -> tuple[list[str], list[str]]:
|
|
206
|
+
"""``(affected_files, departments)`` exactly as Forge step 3 estimates them.
|
|
207
|
+
|
|
208
|
+
Both estimators are ``ForgeOrchestrator`` methods that read no instance
|
|
209
|
+
state, so a bare instance (no ``__init__``, no dispatcher) runs the
|
|
210
|
+
real code without its side effects.
|
|
211
|
+
"""
|
|
212
|
+
from core.forge.orchestrator import ForgeOrchestrator
|
|
213
|
+
|
|
214
|
+
bare = ForgeOrchestrator.__new__(ForgeOrchestrator)
|
|
215
|
+
return bare._estimate_affected_files(prompt), bare._estimate_departments(prompt)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _heuristic_forge_departments(case: ReplayCase) -> list[str]:
|
|
219
|
+
return sorted(forge_estimates(case.prompt)[1])
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _heuristic_forge_complexity(case: ReplayCase) -> dict[str, int]:
|
|
223
|
+
# No similar plans / reused patterns: replay has no plan history, so
|
|
224
|
+
# novelty scores as the heuristic's maximum (90) on every case.
|
|
225
|
+
from core.forge.complexity import score_dimensions
|
|
226
|
+
|
|
227
|
+
files, departments = forge_estimates(case.prompt)
|
|
228
|
+
return score_dimensions(case.prompt, files, departments, [], []).model_dump()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@lru_cache(maxsize=1)
|
|
232
|
+
def registry_commands() -> tuple[dict[str, object], ...]:
|
|
233
|
+
"""``knowledge/commands-registry.json`` commands (read once)."""
|
|
234
|
+
path = repo_root() / "knowledge" / "commands-registry.json"
|
|
235
|
+
return tuple(json.loads(path.read_text(encoding="utf-8"))["commands"])
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def skill_candidates_for(prompt: str, dept: str | None = None) -> list[dict[str, str]]:
|
|
239
|
+
"""The skill-hints menu, built by the SAME function the UPS hook uses
|
|
240
|
+
(``core.synapse.command_menu.skill_hint_candidates``): top-20 keyword
|
|
241
|
+
commands + the routed department's commands, capped at 60.
|
|
242
|
+
|
|
243
|
+
``dept`` is the department the route site settled on; None falls back
|
|
244
|
+
to the L1 keyword route, which is what the hook uses when the route
|
|
245
|
+
site does not act.
|
|
246
|
+
"""
|
|
247
|
+
from core.synapse.command_menu import skill_hint_candidates
|
|
248
|
+
from core.synapse.layers import keyword_department
|
|
249
|
+
|
|
250
|
+
routed = (keyword_department(prompt) or "") if dept is None else dept
|
|
251
|
+
return skill_hint_candidates(registry_commands(), prompt, routed)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def replay_route(case: ReplayCase) -> str | None:
|
|
255
|
+
"""The route the skill-hints replay assumes: the expected command's department.
|
|
256
|
+
|
|
257
|
+
The replay scores the skill-hints site on its own job (pick the command
|
|
258
|
+
from the menu), with the route taken as correct; the route site has its
|
|
259
|
+
own replay (90.9 % on 2026-09-23), and end-to-end coverage in the hook
|
|
260
|
+
is bounded by it. A case whose expected answer is "no command" keeps
|
|
261
|
+
the keyword route (None).
|
|
262
|
+
"""
|
|
263
|
+
from core.synapse.command_menu import command_department
|
|
264
|
+
|
|
265
|
+
for command in registry_commands():
|
|
266
|
+
if command.get("id") == case.expected:
|
|
267
|
+
return command_department(command) or None
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _heuristic_skill_hint(case: ReplayCase) -> str:
|
|
272
|
+
# The live L5 hint is keyword-only: the routed-department fill of the
|
|
273
|
+
# menu never becomes a hint by itself.
|
|
274
|
+
from core.synapse.layers import _score_commands
|
|
275
|
+
|
|
276
|
+
top = _score_commands(list(registry_commands()), case.prompt.lower())
|
|
277
|
+
ids = {str(c.get("command", "")): str(c["id"]) for c in reversed(registry_commands())}
|
|
278
|
+
return ids.get(top[0][1], "") if top else ""
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _heuristic_phantom(case: ReplayCase) -> bool:
|
|
282
|
+
return gov.heuristic_unbacked_effect(case.prompt, case.context.get("tool_uses"))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _heuristic_ui(case: ReplayCase) -> bool:
|
|
286
|
+
return gov.heuristic_ui_code(str(case.context.get("path", "")), case.prompt)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
HEURISTICS: dict[str, Callable[[ReplayCase], object]] = {
|
|
290
|
+
"topic-drift": _heuristic_topic_drift,
|
|
291
|
+
"refine": _heuristic_refine,
|
|
292
|
+
"creation-intent": _heuristic_creation,
|
|
293
|
+
"route": _heuristic_route,
|
|
294
|
+
"bash-effect": _heuristic_bash_effect,
|
|
295
|
+
"forge-departments": _heuristic_forge_departments,
|
|
296
|
+
"forge-complexity": _heuristic_forge_complexity,
|
|
297
|
+
"skill-hints": _heuristic_skill_hint,
|
|
298
|
+
"dispatch-role": lambda case: keyword_dispatch_role(case.prompt),
|
|
299
|
+
"subagent-discipline": lambda case: keyword_needs_isolation(case.prompt),
|
|
300
|
+
"sycophancy": lambda case: gov.heuristic_sycophantic(case.prompt),
|
|
301
|
+
"phantom-action": _heuristic_phantom,
|
|
302
|
+
"skill-proposer": lambda case: gov.heuristic_repeatable_capability(case.prompt),
|
|
303
|
+
"learning-signal": lambda case: gov.heuristic_learning_signal(case.prompt),
|
|
304
|
+
"ui-in-ts": _heuristic_ui,
|
|
305
|
+
"qg-prescreen": lambda case: quality.prescreen_heuristic(),
|
|
306
|
+
"slop-score": lambda case: None, # no slop heuristic exists in code
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _forge_case_state(case: ReplayCase) -> State:
|
|
311
|
+
files, departments = forge_estimates(case.prompt)
|
|
312
|
+
return forge_state(case.prompt, files, departments)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _sycophancy_state(case: ReplayCase) -> State:
|
|
316
|
+
return gov.stop_state(case.prompt, user_message=case.prior[-1] if case.prior else "")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _phantom_state(case: ReplayCase) -> State:
|
|
320
|
+
tool_uses = case.context.get("tool_uses")
|
|
321
|
+
return gov.stop_state(case.prompt, tool_uses=tool_uses if isinstance(tool_uses, int) else None)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
_GIT_B_PATH = re.compile(r"^diff --git a/\S+ b/(\S+)$", re.M)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _diff_path(diff: str) -> str:
|
|
328
|
+
"""The file a corpus diff names first ('' when none: privacy then refuses it)."""
|
|
329
|
+
m = _GIT_B_PATH.search(diff)
|
|
330
|
+
return m.group(1) if m else ""
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
STATES: dict[str, Callable[[ReplayCase], State]] = {
|
|
334
|
+
"bash-effect": lambda case: command_state(case.prompt),
|
|
335
|
+
"forge-departments": _forge_case_state,
|
|
336
|
+
"forge-complexity": _forge_case_state,
|
|
337
|
+
"skill-hints": lambda case: skill_state(
|
|
338
|
+
case.prompt, skill_candidates_for(case.prompt, replay_route(case))),
|
|
339
|
+
"sycophancy": _sycophancy_state,
|
|
340
|
+
"phantom-action": _phantom_state,
|
|
341
|
+
"skill-proposer": lambda case: gov.stop_state(case.prompt),
|
|
342
|
+
"learning-signal": lambda case: gov.stop_state("", user_message=case.prompt),
|
|
343
|
+
"ui-in-ts": lambda case: gov.ui_state(str(case.context.get("path", "")), case.prompt),
|
|
344
|
+
"qg-prescreen": lambda case: quality.diff_state(case.prompt, _diff_path(case.prompt)),
|
|
345
|
+
# A corpus row is prose, as the live site's .md/.mdx/.txt files are.
|
|
346
|
+
"slop-score": lambda case: quality.prose_state(case.prompt, f"{case.id}.md"),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def case_state(site: str, case: ReplayCase) -> State:
|
|
351
|
+
"""The state the live call site would send for this case."""
|
|
352
|
+
builder = STATES.get(site)
|
|
353
|
+
return builder(case) if builder else prompt_state(case.prompt, case.prior)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# --- corpus labels -----------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
TIERS = frozenset({"SHALLOW", "STANDARD", "DEEP"})
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _is_department(value: object) -> bool:
|
|
362
|
+
from core.synapse.layers import DEPARTMENT_PATTERNS
|
|
363
|
+
|
|
364
|
+
return isinstance(value, str) and value in DEPARTMENT_PATTERNS
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _is_department_list(value: object) -> bool:
|
|
368
|
+
return (isinstance(value, list) and 1 <= len(value) <= 4
|
|
369
|
+
and len(set(value)) == len(value) and all(_is_department(v) for v in value))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _is_role(value: object) -> bool:
|
|
373
|
+
from core.runtime.model_router import ROLE_DESCRIPTIONS
|
|
374
|
+
|
|
375
|
+
return isinstance(value, str) and value in ROLE_DESCRIPTIONS
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _is_command_id(value: object) -> bool:
|
|
379
|
+
return isinstance(value, str) and value in {c.get("id") for c in registry_commands()}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _is_bool(value: object) -> bool:
|
|
383
|
+
return isinstance(value, bool)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _is_prescreen_label(value: object) -> bool:
|
|
387
|
+
"""``[verdict, blocker]``: approved pairs with none, rejected with a real class."""
|
|
388
|
+
if not (isinstance(value, list) and len(value) == 2):
|
|
389
|
+
return False
|
|
390
|
+
pair: list[object] = value
|
|
391
|
+
verdict, blocker = pair
|
|
392
|
+
if verdict == "approved":
|
|
393
|
+
return blocker == quality.NO_BLOCKER
|
|
394
|
+
return verdict == "rejected" and blocker in set(quality.BLOCKERS) - {quality.NO_BLOCKER}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _is_slop_label(value: object) -> bool:
|
|
398
|
+
"""Five ints in 1..10, rubric order."""
|
|
399
|
+
return (isinstance(value, list) and len(value) == len(quality.SLOP_DIMENSIONS)
|
|
400
|
+
and all(isinstance(v, int) and not isinstance(v, bool)
|
|
401
|
+
and 1 <= v <= quality.SLOP_LEVELS for v in value))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
EXPECTED: dict[str, Callable[[object], bool]] = {
|
|
405
|
+
"topic-drift": _is_bool, "refine": _is_bool, "creation-intent": _is_bool,
|
|
406
|
+
"bash-effect": _is_bool, "subagent-discipline": _is_bool,
|
|
407
|
+
"route": lambda v: v == "" or _is_department(v),
|
|
408
|
+
"dispatch-role": _is_role,
|
|
409
|
+
"forge-complexity": lambda v: isinstance(v, str) and v in TIERS,
|
|
410
|
+
"forge-departments": _is_department_list,
|
|
411
|
+
"skill-hints": _is_command_id,
|
|
412
|
+
"sycophancy": _is_bool, "phantom-action": _is_bool, "skill-proposer": _is_bool,
|
|
413
|
+
"ui-in-ts": _is_bool,
|
|
414
|
+
"learning-signal": lambda v: isinstance(v, str) and v in gov.LEARNING_SIGNALS,
|
|
415
|
+
"qg-prescreen": _is_prescreen_label,
|
|
416
|
+
"slop-score": _is_slop_label,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def complexity_tier(value: object) -> object:
|
|
421
|
+
"""Dimensions (0-100 each) → ``SHALLOW|STANDARD|DEEP`` with the Forge's weights."""
|
|
422
|
+
if not isinstance(value, dict):
|
|
423
|
+
return value
|
|
424
|
+
from core.forge.complexity import calculate_weighted_score, determine_tier
|
|
425
|
+
from core.forge.schema import ComplexityDimensions
|
|
426
|
+
|
|
427
|
+
tier = determine_tier(calculate_weighted_score(ComplexityDimensions(**value)))
|
|
428
|
+
return tier.value.upper()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _department_set(value: object) -> object:
|
|
432
|
+
return sorted(value) if isinstance(value, list) else value
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _learning_label(value: object) -> object:
|
|
436
|
+
return value.get("signal") if isinstance(value, dict) else value
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _prescreen_label(value: object) -> object:
|
|
440
|
+
if not isinstance(value, dict):
|
|
441
|
+
return value
|
|
442
|
+
return [value.get("verdict"), value.get("blocker")]
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _slop_label(value: object) -> object:
|
|
446
|
+
if not isinstance(value, dict):
|
|
447
|
+
return value
|
|
448
|
+
return [value.get(d) for d in quality.SLOP_DIMENSIONS]
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
LABELS: dict[str, Callable[[object], object]] = {
|
|
452
|
+
"forge-complexity": complexity_tier,
|
|
453
|
+
"forge-departments": _department_set,
|
|
454
|
+
"learning-signal": _learning_label,
|
|
455
|
+
"qg-prescreen": _prescreen_label,
|
|
456
|
+
"slop-score": _slop_label,
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def default_corpus_path(site: str) -> Path:
|
|
461
|
+
"""``config/decisions/corpora/<site>.jsonl`` in this checkout."""
|
|
462
|
+
return repo_root() / "config" / "decisions" / "corpora" / f"{site}.jsonl"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def heldout_corpus_path(site: str) -> Path:
|
|
466
|
+
"""``config/decisions/corpora/heldout/<site>.jsonl`` in this checkout."""
|
|
467
|
+
return repo_root() / "config" / "decisions" / "corpora" / "heldout" / f"{site}.jsonl"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _gap_error(site: str, case: ReplayCase) -> str | None:
|
|
471
|
+
"""Why ``case.gap`` is invalid for ``site``, or None."""
|
|
472
|
+
if case.gap is None:
|
|
473
|
+
return None
|
|
474
|
+
if site != "refine":
|
|
475
|
+
return f"gap is a refine field, not a {site} one"
|
|
476
|
+
from core.decisions.sites.prompt import REFINE_GAPS
|
|
477
|
+
|
|
478
|
+
if case.gap not in REFINE_GAPS:
|
|
479
|
+
return f"gap {case.gap!r} is not one of {sorted(REFINE_GAPS)}"
|
|
480
|
+
if (case.gap == "none") is bool(case.expected):
|
|
481
|
+
return f"gap {case.gap!r} contradicts expected {case.expected!r}"
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def load_corpus(site: str, path: Path | None = None) -> list[ReplayCase]:
|
|
486
|
+
"""Parse a JSONL corpus; ValueError names the first bad line."""
|
|
487
|
+
source = path or default_corpus_path(site)
|
|
488
|
+
check = EXPECTED.get(site)
|
|
489
|
+
cases: list[ReplayCase] = []
|
|
490
|
+
for number, line in enumerate(source.read_text(encoding="utf-8").splitlines(), 1):
|
|
491
|
+
if not line.strip():
|
|
492
|
+
continue
|
|
493
|
+
try:
|
|
494
|
+
case = ReplayCase.model_validate(json.loads(line))
|
|
495
|
+
except (ValueError, ValidationError) as exc:
|
|
496
|
+
raise ValueError(f"{source}:{number}: {exc}") from exc
|
|
497
|
+
if check is not None and not check(case.expected):
|
|
498
|
+
raise ValueError(f"{source}:{number}: expected {case.expected!r} is not a {site} label")
|
|
499
|
+
gap_error = _gap_error(site, case)
|
|
500
|
+
if gap_error:
|
|
501
|
+
raise ValueError(f"{source}:{number}: {gap_error}")
|
|
502
|
+
cases.append(case)
|
|
503
|
+
return cases
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# --- scoring -----------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
Row = tuple[ReplayCase, object, Outcome | None]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _labelled(site: str, rows: Sequence[Row]) -> list[Row]:
|
|
512
|
+
label = LABELS.get(site)
|
|
513
|
+
if label is None:
|
|
514
|
+
return list(rows)
|
|
515
|
+
# Both sides go through the label: a department list is compared as a
|
|
516
|
+
# set, so the corpus order must not matter either.
|
|
517
|
+
return [(case.model_copy(update={"expected": label(case.expected)}), label(h),
|
|
518
|
+
_label_outcome(label, out)) for case, h, out in rows]
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _label_outcome(label: Callable[[object], object], out: Outcome | None) -> Outcome | None:
|
|
522
|
+
if out is None:
|
|
523
|
+
return None
|
|
524
|
+
jev = None if out.jev is None else label(out.jev)
|
|
525
|
+
return replace(out, value=label(out.value), heuristic=label(out.heuristic), jev=jev)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _rate(hits: int, total: int) -> float | None:
|
|
529
|
+
return round(hits / total, 4) if total else None
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _accuracy(rows: Sequence[Row], pick: Callable[[Row], object]) -> float | None:
|
|
533
|
+
return _rate(sum(1 for r in rows if pick(r) == r[0].expected), len(rows))
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _answered(rows: Sequence[Row]) -> list[Row]:
|
|
537
|
+
return [r for r in rows if r[2] is not None and r[2].jev is not None]
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _false_escalation(site: Site, rows: Sequence[Row]) -> float | None:
|
|
541
|
+
if site.direction != "escalate_only":
|
|
542
|
+
return None
|
|
543
|
+
at_risk = [r for r in rows if r[1] == r[0].expected]
|
|
544
|
+
wrong = [r for r in at_risk if r[2] is not None and r[2].acted_on == "jev"
|
|
545
|
+
and r[2].value != r[0].expected]
|
|
546
|
+
return _rate(len(wrong), len(at_risk))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _by_lang(rows: Sequence[Row], online: bool) -> dict[str, dict[str, float | None]]:
|
|
550
|
+
out: dict[str, dict[str, float | None]] = {}
|
|
551
|
+
for lang in sorted({r[0].lang for r in rows}):
|
|
552
|
+
subset = [r for r in rows if r[0].lang == lang]
|
|
553
|
+
entry = {"cases": float(len(subset)), "heuristic": _accuracy(subset, lambda r: r[1])}
|
|
554
|
+
if online:
|
|
555
|
+
entry["jev"] = _accuracy(_answered(subset), lambda r: r[2].jev if r[2] else None)
|
|
556
|
+
out[lang] = entry
|
|
557
|
+
return out
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _base_report(site: Site, rows: Sequence[Row]) -> ReplayReport:
|
|
561
|
+
pt = sum(1 for r in rows if r[0].lang == "pt")
|
|
562
|
+
return ReplayReport(
|
|
563
|
+
site=site.name, cases=len(rows), pt_share=_rate(pt, len(rows)) or 0.0,
|
|
564
|
+
heuristic_accuracy=_accuracy(rows, lambda r: r[1]) or 0.0,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _online_scores(site: Site, rows: Sequence[Row], report: ReplayReport) -> None:
|
|
569
|
+
answered = _answered(rows)
|
|
570
|
+
report.jev_accuracy = _accuracy(answered, lambda r: r[2].jev if r[2] else None)
|
|
571
|
+
report.heuristic_on_answered = _accuracy(answered, lambda r: r[1])
|
|
572
|
+
report.act_accuracy = _accuracy(rows, lambda r: r[2].value if r[2] else r[1])
|
|
573
|
+
report.abstain_rate = _rate(len(rows) - len(answered), len(rows))
|
|
574
|
+
report.false_escalation_rate = _false_escalation(site, rows)
|
|
575
|
+
if site.name in MAE_SITES:
|
|
576
|
+
report.mean_abs_error = _mean_abs_error(answered)
|
|
577
|
+
# "no-questions" is a site choosing not to ask (phantom-action with tools on
|
|
578
|
+
# record, a skill bypass marker): an abstain by design, not an outage.
|
|
579
|
+
report.unavailable = sum(1 for r in rows if r[2] is None or r[2].reason not in
|
|
580
|
+
("jev", "abstain", "shadow", "downgrade-blocked", "no-questions"))
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _total(value: object) -> int | None:
|
|
584
|
+
if not isinstance(value, list) or not all(isinstance(v, int) for v in value):
|
|
585
|
+
return None
|
|
586
|
+
return sum(value)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _mean_abs_error(answered: Sequence[Row]) -> float | None:
|
|
590
|
+
"""Mean |Jev total - labelled total| over the answered rows (5-list labels)."""
|
|
591
|
+
errors = []
|
|
592
|
+
for case, _, out in answered:
|
|
593
|
+
jev, want = _total(out.jev if out else None), _total(case.expected)
|
|
594
|
+
if jev is not None and want is not None:
|
|
595
|
+
errors.append(abs(jev - want))
|
|
596
|
+
return round(statistics.fmean(errors), 4) if errors else None
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _secondary_scores(site: Site, rows: Sequence[Row], report: ReplayReport,
|
|
600
|
+
threshold: float) -> None:
|
|
601
|
+
"""Scores reported next to the gate, never part of it."""
|
|
602
|
+
if site.name == "refine":
|
|
603
|
+
report.gap_accuracy = _gap_accuracy(rows)
|
|
604
|
+
reader = ALT_READERS.get(site.name)
|
|
605
|
+
if reader is not None:
|
|
606
|
+
answered = sum(1 for _, _, out in rows
|
|
607
|
+
if out is not None and reader(dict(out.answers), threshold) is not None)
|
|
608
|
+
report.single_cell_abstain_rate = _rate(len(rows) - answered, len(rows))
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _gap_accuracy(rows: Sequence[Row]) -> float | None:
|
|
612
|
+
"""Jev's ``missing`` choice vs the labelled ``gap``, where both exist."""
|
|
613
|
+
pairs = [(case.gap, out.answers["missing"].choice) for case, _, out in rows
|
|
614
|
+
if case.gap is not None and out is not None and "missing" in out.answers
|
|
615
|
+
and out.answers["missing"].choice]
|
|
616
|
+
return _rate(sum(1 for want, got in pairs if want == got), len(pairs))
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# D6: the pre-window reading of a site, run on the same answers for the report.
|
|
620
|
+
ALT_READERS: dict[str, Callable[[dict[str, Answer], float], object | None]] = {
|
|
621
|
+
"forge-complexity": single_cell_complexity,
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _corpus_failures(report: ReplayReport) -> list[str]:
|
|
626
|
+
failures = []
|
|
627
|
+
floor = MIN_HELDOUT_CASES if report.corpus == "heldout" else MIN_CASES
|
|
628
|
+
if report.cases < floor:
|
|
629
|
+
failures.append(f"corpus has {report.cases} cases (< {floor})")
|
|
630
|
+
if report.pt_share < MIN_PT_SHARE:
|
|
631
|
+
failures.append(f"pt-PT share {report.pt_share:.0%} (< {MIN_PT_SHARE:.0%})")
|
|
632
|
+
return failures
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _precision_failures(report: ReplayReport) -> list[str]:
|
|
636
|
+
jev, base = report.jev_accuracy, report.heuristic_on_answered
|
|
637
|
+
if jev is None or base is None:
|
|
638
|
+
return ["Jev answered no case"]
|
|
639
|
+
if report.site in MAE_SITES:
|
|
640
|
+
mae = report.mean_abs_error
|
|
641
|
+
if mae is None or mae > MAX_SLOP_MAE:
|
|
642
|
+
return [f"mean absolute error {mae} > {MAX_SLOP_MAE} (total, 5-50)"]
|
|
643
|
+
return []
|
|
644
|
+
if jev < base + MARGIN:
|
|
645
|
+
return [f"Jev precision {jev:.1%} < heuristic {base:.1%} + {MARGIN:.0%}"]
|
|
646
|
+
return []
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _gate_failures(report: ReplayReport) -> list[str]:
|
|
650
|
+
failures = _precision_failures(report)
|
|
651
|
+
if report.abstain_rate is not None and report.abstain_rate > MAX_ABSTAIN:
|
|
652
|
+
failures.append(f"abstain {report.abstain_rate:.1%} > {MAX_ABSTAIN:.0%}")
|
|
653
|
+
fe = report.false_escalation_rate
|
|
654
|
+
if fe is not None and fe > MAX_FALSE_ESCALATION:
|
|
655
|
+
failures.append(f"false escalation {fe:.1%} > {MAX_FALSE_ESCALATION:.0%}")
|
|
656
|
+
return failures
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# Reasons decided on this machine, before any byte left it.
|
|
660
|
+
LOCAL_REASONS = frozenset({"cache-hit", "no-questions", "deadline", "backoff", "egress-denied"})
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def reached_endpoint(reason: str) -> bool:
|
|
664
|
+
"""True when the endpoint decided the call (answered, or erred there).
|
|
665
|
+
|
|
666
|
+
Not reached: a :data:`LOCAL_REASONS` head, and any ``<reason>:local``
|
|
667
|
+
(:data:`~core.decisions.client.LOCAL_MARK`): ``invalid-shape:local``
|
|
668
|
+
(a state or body that never serialised) and ``timeout:local`` (our own
|
|
669
|
+
wall-clock deadline, not an answer or an error from the endpoint).
|
|
670
|
+
"""
|
|
671
|
+
head, _, cause = reason.partition(":")
|
|
672
|
+
return head not in LOCAL_REASONS and cause != LOCAL_MARK
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@dataclass(frozen=True)
|
|
676
|
+
class CallSample:
|
|
677
|
+
"""What one replay call cost: latency when it went out, and USD."""
|
|
678
|
+
|
|
679
|
+
latency_ms: int | None
|
|
680
|
+
cost_usd: float
|
|
681
|
+
reached: bool = False
|
|
682
|
+
asked: bool = True
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _ask(
|
|
686
|
+
site: Site,
|
|
687
|
+
case: ReplayCase,
|
|
688
|
+
heuristic: object,
|
|
689
|
+
transport: Transport,
|
|
690
|
+
cfg: DecisionsConfig,
|
|
691
|
+
timeout_ms: int,
|
|
692
|
+
) -> tuple[Outcome, CallSample]:
|
|
693
|
+
call = SiteCall(site, heuristic)
|
|
694
|
+
state = case_state(site.name, case)
|
|
695
|
+
response, reason, latency = run_sync(
|
|
696
|
+
[call], state, transport=transport,
|
|
697
|
+
cfg=cfg, timeout_s=timeout_ms / 1000, session_id=REPLAY_SESSION_ID,
|
|
698
|
+
cost_category=REPLAY_COST_CATEGORY, mark_local=True,
|
|
699
|
+
)
|
|
700
|
+
outcome = resolve(call, response, "act", threshold_for(cfg, site), reason, state=state)
|
|
701
|
+
went_out = reason != "cache-hit" and not reason.startswith("backoff")
|
|
702
|
+
fresh = reason == "ok" and response is not None
|
|
703
|
+
cost = (call_cost_usd(transport, response.usage) or 0.0) if fresh and response else 0.0
|
|
704
|
+
sample = CallSample(latency if went_out else None, cost,
|
|
705
|
+
reached=reached_endpoint(reason), asked=reason != "no-questions")
|
|
706
|
+
return outcome, sample
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _call_scores(samples: Sequence[CallSample], report: ReplayReport) -> None:
|
|
710
|
+
report.asked = sum(1 for s in samples if s.asked)
|
|
711
|
+
report.reached = sum(1 for s in samples if s.asked and s.reached)
|
|
712
|
+
latencies = [s.latency_ms for s in samples if s.latency_ms is not None]
|
|
713
|
+
if not latencies:
|
|
714
|
+
report.p50_latency_ms = report.cost_usd = None
|
|
715
|
+
return
|
|
716
|
+
report.p50_latency_ms = int(statistics.median(latencies))
|
|
717
|
+
report.cost_usd = round(sum(s.cost_usd for s in samples), 10)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _score_cases(
|
|
721
|
+
site: Site,
|
|
722
|
+
cases: Sequence[ReplayCase],
|
|
723
|
+
transport: Transport | None,
|
|
724
|
+
cfg: DecisionsConfig,
|
|
725
|
+
ceiling_ms: int,
|
|
726
|
+
) -> tuple[list[Row], list[CallSample]]:
|
|
727
|
+
"""The heuristic for every case and, with a transport, Jev's outcome."""
|
|
728
|
+
heuristic = HEURISTICS[site.name]
|
|
729
|
+
rows: list[Row] = []
|
|
730
|
+
samples: list[CallSample] = []
|
|
731
|
+
for case in cases:
|
|
732
|
+
h = heuristic(case)
|
|
733
|
+
outcome = None
|
|
734
|
+
if transport is not None:
|
|
735
|
+
outcome, sample = _ask(site, case, h, transport, cfg, ceiling_ms)
|
|
736
|
+
samples.append(sample)
|
|
737
|
+
rows.append((case, h, outcome))
|
|
738
|
+
return rows, samples
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def replay(
|
|
742
|
+
site: str,
|
|
743
|
+
cases: Sequence[ReplayCase],
|
|
744
|
+
*,
|
|
745
|
+
transport: Transport | None,
|
|
746
|
+
offline: bool = False,
|
|
747
|
+
timeout_ms: int | None = None,
|
|
748
|
+
cfg: DecisionsConfig | None = None,
|
|
749
|
+
corpus: CorpusKind = "corpus",
|
|
750
|
+
) -> ReplayReport:
|
|
751
|
+
"""Score ``cases`` for ``site``; offline (or no transport) = heuristic only.
|
|
752
|
+
|
|
753
|
+
Each online call is capped at ``timeout_ms``, else at the site's own
|
|
754
|
+
ceiling: the replay cuts a call where the live site would. ``cfg``
|
|
755
|
+
defaults to the operator's config (its thresholds are what ships);
|
|
756
|
+
``replay_report`` passes a copy with the answer cache off. ``corpus=
|
|
757
|
+
"heldout"`` marks a held-out set (its own, smaller size floor).
|
|
758
|
+
"""
|
|
759
|
+
target = SITES[site]
|
|
760
|
+
online = not offline and transport is not None
|
|
761
|
+
cfg = cfg or load_decisions_config()
|
|
762
|
+
ceiling = timeout_ms or site_timeout_ms(cfg, target)
|
|
763
|
+
rows, samples = _score_cases(target, cases, transport if online else None, cfg, ceiling)
|
|
764
|
+
rows = _labelled(site, rows)
|
|
765
|
+
report = _base_report(target, rows)
|
|
766
|
+
report.corpus = corpus
|
|
767
|
+
report.by_lang = _by_lang(rows, online)
|
|
768
|
+
if online:
|
|
769
|
+
report.timeout_ms = ceiling
|
|
770
|
+
_online_scores(target, rows, report)
|
|
771
|
+
_secondary_scores(target, rows, report, threshold_for(cfg, target))
|
|
772
|
+
_call_scores(samples, report)
|
|
773
|
+
return _judged(report, online)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _judged(report: ReplayReport, online: bool) -> ReplayReport:
|
|
777
|
+
report.failures = _corpus_failures(report) + (_gate_failures(report) if online else [])
|
|
778
|
+
report.gate = "fail" if report.failures else ("pass" if online else "offline")
|
|
779
|
+
return report
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _call_line(report: ReplayReport) -> str:
|
|
783
|
+
if report.abstain_rate is None: # set only by an online run
|
|
784
|
+
return "- p50 latency / cost: not measured (offline)"
|
|
785
|
+
if report.p50_latency_ms is None or report.cost_usd is None:
|
|
786
|
+
return "- p50 latency / cost: not measured (all cache hits)"
|
|
787
|
+
return f"- p50 latency: {report.p50_latency_ms} ms; cost: ${report.cost_usd:.6f}"
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _render(report: ReplayReport) -> str:
|
|
791
|
+
kind = ", held-out" if report.corpus == "heldout" else ""
|
|
792
|
+
lines = [f"# Replay — {report.site} ({report.gate}{kind})",
|
|
793
|
+
f"- cases: {report.cases} (pt-PT {report.pt_share:.0%})",
|
|
794
|
+
f"- heuristic accuracy: {report.heuristic_accuracy:.1%}"]
|
|
795
|
+
if report.jev_accuracy is not None:
|
|
796
|
+
lines += [f"- Jev precision (answered): {report.jev_accuracy:.1%} vs heuristic "
|
|
797
|
+
f"{(report.heuristic_on_answered or 0):.1%} on the same cases",
|
|
798
|
+
f"- act accuracy: {(report.act_accuracy or 0):.1%}",
|
|
799
|
+
f"- abstain: {(report.abstain_rate or 0):.1%} (unavailable {report.unavailable})"]
|
|
800
|
+
lines.append(_call_line(report))
|
|
801
|
+
if report.false_escalation_rate is not None:
|
|
802
|
+
lines.append(f"- false escalation: {report.false_escalation_rate:.1%}")
|
|
803
|
+
if report.mean_abs_error is not None:
|
|
804
|
+
lines.append(f"- mean absolute error (total, 5-50): {report.mean_abs_error}")
|
|
805
|
+
if report.gap_accuracy is not None:
|
|
806
|
+
lines.append(f"- missing-piece choice vs gap label: {report.gap_accuracy:.1%}")
|
|
807
|
+
if report.single_cell_abstain_rate is not None:
|
|
808
|
+
lines.append(f"- abstain with the single-cell reading (pre-D6): "
|
|
809
|
+
f"{report.single_cell_abstain_rate:.1%}")
|
|
810
|
+
for lang, scores in report.by_lang.items():
|
|
811
|
+
lines.append(f"- {lang}: " + ", ".join(f"{k}={v}" for k, v in scores.items()))
|
|
812
|
+
lines += [f"- FAIL: {f}" for f in report.failures]
|
|
813
|
+
return "\n".join(lines)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _parse_args(argv: Sequence[str] | None) -> argparse.Namespace:
|
|
817
|
+
parser = argparse.ArgumentParser(prog="python -m core.decisions.replay")
|
|
818
|
+
parser.add_argument("--site", required=True, choices=sorted(SITES))
|
|
819
|
+
parser.add_argument("--corpus", type=Path, default=None)
|
|
820
|
+
parser.add_argument("--heldout", action="store_true",
|
|
821
|
+
help="replay the held-out set (corpora/heldout/<site>.jsonl)")
|
|
822
|
+
parser.add_argument("--offline", action="store_true")
|
|
823
|
+
parser.add_argument("--json", action="store_true")
|
|
824
|
+
parser.add_argument("--timeout-ms", type=int, default=None,
|
|
825
|
+
help="per-call ceiling (default: the site's own timeout_ms)")
|
|
826
|
+
return parser.parse_args(argv)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
830
|
+
"""CLI: exit 0 pass/offline-valid, 1 gate failed, 2 usage error."""
|
|
831
|
+
try:
|
|
832
|
+
ns = _parse_args(argv)
|
|
833
|
+
except SystemExit as exc:
|
|
834
|
+
return 0 if exc.code == 0 else 2
|
|
835
|
+
heldout = ns.heldout and ns.corpus is None
|
|
836
|
+
try:
|
|
837
|
+
cases = load_corpus(ns.site, heldout_corpus_path(ns.site) if heldout else ns.corpus)
|
|
838
|
+
except (OSError, ValueError) as exc:
|
|
839
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
840
|
+
return 2
|
|
841
|
+
transport = None if ns.offline else resolve_transport(load_decisions_config())
|
|
842
|
+
if not ns.offline and transport is None:
|
|
843
|
+
print("error: no transport (set OPENROUTER_API_KEY) — or pass --offline", file=sys.stderr)
|
|
844
|
+
return 2
|
|
845
|
+
report = replay(ns.site, cases, transport=transport, offline=ns.offline,
|
|
846
|
+
timeout_ms=ns.timeout_ms, corpus="heldout" if ns.heldout else "corpus")
|
|
847
|
+
print(json.dumps(asdict(report), ensure_ascii=False, indent=2) if ns.json else _render(report))
|
|
848
|
+
return 0 if report.gate != "fail" else 1
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
if __name__ == "__main__":
|
|
852
|
+
raise SystemExit(main())
|