@mutagent/evaluator 0.1.0-alpha.4 → 0.1.0-alpha.6
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/.claude/skills/mutagent-evaluator/SKILL.md +6 -5
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +76 -12
- package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
- package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +1 -1
- package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
- package/.claude/skills/mutagent-evaluator/references/eval-stage.md +36 -3
- package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
- package/.claude/skills/mutagent-evaluator/references/operation-inventory.md +1 -0
- package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
- package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
- package/.claude/skills/mutagent-evaluator/scripts/cli/audit-run.ts +6 -3
- package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
- package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +277 -0
- package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +39 -0
- package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
- package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
- package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
- package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
- package/.claude/skills/mutagent-evaluator/scripts/persist-eval-criteria.ts +232 -0
- package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
- package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
- package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
- package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +9 -91
- package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +14 -4
- package/.claude/skills/mutagent-evaluator/scripts/run-deterministic.ts +114 -18
- package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
- package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
- package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +244 -0
- package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
- package/bin/mutagent-cli.mjs +659 -8
- package/package.json +2 -2
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
import type { JudgeInvoke } from "./determine-outcome.ts";
|
|
23
23
|
import { determineOutcome, runJudge } from "./judge-prompt-template.ts";
|
|
24
24
|
import { profileSubject, type SubjectProfile } from "./profile-subject.ts";
|
|
25
|
+
import { buildSubjectProfile, deriveSubjectFrame } from "./subject-profile.ts";
|
|
26
|
+
import {
|
|
27
|
+
normativeCriterionIds,
|
|
28
|
+
pendingRatifications,
|
|
29
|
+
type CalibrationDecision,
|
|
30
|
+
} from "./memory/ratify.ts";
|
|
25
31
|
import { splitTrainEval, buildJudgeSpec, type JudgePin } from "./build-evals.ts";
|
|
26
32
|
import { rollupScorecard, type GradedCriterion, type Scorecard } from "./evaluate.ts";
|
|
27
33
|
import {
|
|
@@ -64,6 +70,12 @@ export interface PipelineOptions {
|
|
|
64
70
|
severityById?: Record<string, string>;
|
|
65
71
|
/** artifact refs to enumerate on the handoff (caller-injected locators). */
|
|
66
72
|
artifacts?: ArtifactRef[];
|
|
73
|
+
/**
|
|
74
|
+
* EV-4 — prior operator ratify/eliminate decisions (the review UI's `calibration.json`,
|
|
75
|
+
* routed via `memory/ratify.ts`). A NORMATIVE criterion with no `verify` here is
|
|
76
|
+
* `needs-ratification` → the verdict is HELD (surfaced in `pendingRatification`).
|
|
77
|
+
*/
|
|
78
|
+
ratifications?: CalibrationDecision[];
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
export interface PipelineResult {
|
|
@@ -73,6 +85,12 @@ export interface PipelineResult {
|
|
|
73
85
|
verdicts: CriterionVerdict[];
|
|
74
86
|
scorecard: Scorecard;
|
|
75
87
|
handoff: HandoverBundle;
|
|
88
|
+
/**
|
|
89
|
+
* EV-4 — normative criteria still `needs-ratification` (no operator `verify`).
|
|
90
|
+
* NON-EMPTY ⇒ the verdict is HELD: a value call the judge cannot settle is
|
|
91
|
+
* unresolved, so the run's verdict is not yet final.
|
|
92
|
+
*/
|
|
93
|
+
pendingRatification: string[];
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
/** Map a determiner outcome → a sampling LabeledTrace. */
|
|
@@ -101,11 +119,20 @@ export async function runEvalPipeline(
|
|
|
101
119
|
// EV-049 — subject profile (carries the subject vocab the engine reads).
|
|
102
120
|
const profile = profileSubject(traces, opts.vocab);
|
|
103
121
|
|
|
122
|
+
// EV-5 CUT WIRE 2 — build the M1 judge-packet profile ONCE (identity · purpose ·
|
|
123
|
+
// tools + reversibility). This is `buildSubjectProfile`'s first PRODUCTION caller;
|
|
124
|
+
// it flows to the per-criterion judge preamble (EV-5). The determiner (EV-1) reads
|
|
125
|
+
// the SAME frame via `deriveSubjectFrame` so PREP (Stage A) and this pipeline
|
|
126
|
+
// (Stage B) render byte-identical determiner prompts (cross-stage cache parity).
|
|
127
|
+
const m1Profile = buildSubjectProfile({ subjectName: opts.subject.name, traces });
|
|
128
|
+
const subjectFrame = deriveSubjectFrame(opts.subject.name, traces);
|
|
129
|
+
|
|
104
130
|
// EV-042 — determine an outcome per trace (sequential → deterministic order).
|
|
105
|
-
// The determiner reads the subject vocab off the profile (EV-002)
|
|
131
|
+
// The determiner reads the subject vocab off the profile (EV-002) and, EV-1, the
|
|
132
|
+
// subject frame so it derives THIS subject's expected outcome (not email signals).
|
|
106
133
|
const outcomes: OutcomeResult[] = [];
|
|
107
134
|
for (const t of traces) {
|
|
108
|
-
outcomes.push(await determineOutcome(t, judge, profile.vocab));
|
|
135
|
+
outcomes.push(await determineOutcome(t, judge, profile.vocab, subjectFrame));
|
|
109
136
|
}
|
|
110
137
|
const labeled = traces.map((t, i) => toLabeled(t, outcomes[i]));
|
|
111
138
|
|
|
@@ -125,7 +152,7 @@ export async function runEvalPipeline(
|
|
|
125
152
|
const spec = buildJudgeSpec(criterion, exemplarsFromTrain(train), trainIds);
|
|
126
153
|
// judge a representative held-out subject trace (first eval target).
|
|
127
154
|
const subjectTrace = judgeTargets[0]?.trace ?? sample[0]?.trace ?? traces[0];
|
|
128
|
-
const verdict = await runJudge(spec, subjectTrace, judge, opts.pin);
|
|
155
|
+
const verdict = await runJudge(spec, subjectTrace, judge, opts.pin, m1Profile);
|
|
129
156
|
verdicts.push(verdict);
|
|
130
157
|
graded.push({
|
|
131
158
|
criterionId: criterion.id,
|
|
@@ -171,5 +198,13 @@ export async function runEvalPipeline(
|
|
|
171
198
|
producedAt: opts.producedAt,
|
|
172
199
|
});
|
|
173
200
|
|
|
174
|
-
|
|
201
|
+
// EV-4 — a normative criterion (a value/standard call the judge cannot settle)
|
|
202
|
+
// must be operator-RATIFIED before its verdict is final. Compute which normative
|
|
203
|
+
// criteria have no `verify` decision → the verdict is HELD on them.
|
|
204
|
+
const pendingRatification = pendingRatifications(
|
|
205
|
+
normativeCriterionIds(verdicts),
|
|
206
|
+
opts.ratifications ?? [],
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return { profile, outcomes, sample, verdicts, scorecard, handoff, pendingRatification };
|
|
175
210
|
}
|
|
@@ -22,11 +22,62 @@
|
|
|
22
22
|
import {
|
|
23
23
|
PROFILE_UNKNOWN,
|
|
24
24
|
SubjectProfileProvenance,
|
|
25
|
+
ToolReversibility,
|
|
25
26
|
type SubjectProfile,
|
|
27
|
+
type ToolReversibilityEntry,
|
|
28
|
+
type ToolReversibilityValue,
|
|
26
29
|
} from "./contracts/eval-matrix.ts";
|
|
27
30
|
import { inferToolInventory, inferSystemPrompt } from "./profile-subject.ts";
|
|
28
31
|
import type { EvalTrace } from "./contracts/eval-types.ts";
|
|
29
32
|
|
|
33
|
+
// ── EV-5 — tool reversibility classification ─────────────────────────────────
|
|
34
|
+
//
|
|
35
|
+
// LEAN start (operator: "classify its tools by what they do — irreversible external
|
|
36
|
+
// or not; keep the start lean"). A GIVEN classification (from the subject definition)
|
|
37
|
+
// ALWAYS wins; absent, a conservative NAME heuristic flags well-known irreversible-
|
|
38
|
+
// external verbs and well-known read-only verbs, and marks everything else `unknown`
|
|
39
|
+
// (HONEST — never a false "reversible"). The heavier per-trace side-effect harness
|
|
40
|
+
// (decision option b) can supersede this later without changing the field shape.
|
|
41
|
+
|
|
42
|
+
/** Verb stems whose presence in a tool name signals an IRREVERSIBLE EXTERNAL action. */
|
|
43
|
+
const IRREVERSIBLE_VERBS = [
|
|
44
|
+
"send", "email", "post", "publish", "delete", "remove", "destroy", "drop",
|
|
45
|
+
"charge", "pay", "refund", "transfer", "deploy", "release", "submit", "create",
|
|
46
|
+
"insert", "write", "update", "patch", "put", "execute", "provision", "terminate",
|
|
47
|
+
"cancel", "notify", "dispatch", "message", "reply", "comment", "merge", "push",
|
|
48
|
+
];
|
|
49
|
+
/** Verb stems whose presence signals a READ-ONLY / safely-repeatable action. */
|
|
50
|
+
const REVERSIBLE_VERBS = [
|
|
51
|
+
"get", "list", "read", "search", "fetch", "lookup", "view", "query", "find",
|
|
52
|
+
"check", "inspect", "describe", "count", "peek", "preview", "validate",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Classify one tool's reversibility (EV-5). A GIVEN `override` (subject definition)
|
|
57
|
+
* wins; else a conservative name heuristic — irreversible-external verbs first (the
|
|
58
|
+
* safety-relevant class), then read-only verbs, else `unknown` (never guessed). PURE.
|
|
59
|
+
*/
|
|
60
|
+
export function classifyToolReversibility(
|
|
61
|
+
name: string,
|
|
62
|
+
override?: Record<string, ToolReversibilityValue>,
|
|
63
|
+
): ToolReversibilityValue {
|
|
64
|
+
const given = override?.[name];
|
|
65
|
+
if (given !== undefined) return given;
|
|
66
|
+
const n = name.toLowerCase();
|
|
67
|
+
const hits = (verbs: string[]): boolean => verbs.some((v) => n.includes(v));
|
|
68
|
+
if (hits(IRREVERSIBLE_VERBS)) return ToolReversibility.IrreversibleExternal;
|
|
69
|
+
if (hits(REVERSIBLE_VERBS)) return ToolReversibility.Reversible;
|
|
70
|
+
return ToolReversibility.Unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Classify every tool in the inventory (EV-5). Deterministic; preserves order. */
|
|
74
|
+
export function classifyToolList(
|
|
75
|
+
tools: string[],
|
|
76
|
+
override?: Record<string, ToolReversibilityValue>,
|
|
77
|
+
): ToolReversibilityEntry[] {
|
|
78
|
+
return tools.map((name) => ({ name, reversibility: classifyToolReversibility(name, override) }));
|
|
79
|
+
}
|
|
80
|
+
|
|
30
81
|
/** The GIVEN facts a caller with code/metadata access can supply (M1). Any field
|
|
31
82
|
* left absent is RECONSTRUCTED from the trace batch or MARKED `unknown`. */
|
|
32
83
|
export interface GivenSubjectFacts {
|
|
@@ -41,6 +92,9 @@ export interface GivenSubjectFacts {
|
|
|
41
92
|
version?: string;
|
|
42
93
|
/** the agent's system prompt (code access). Rendered COLLAPSED; never confabulated. */
|
|
43
94
|
systemPrompt?: string;
|
|
95
|
+
/** EV-5 — a GIVEN per-tool reversibility classification (from the subject definition);
|
|
96
|
+
* WINS over the name heuristic. Any tool not named here falls back to the heuristic. */
|
|
97
|
+
toolReversibility?: Record<string, ToolReversibilityValue>;
|
|
44
98
|
}
|
|
45
99
|
|
|
46
100
|
export interface BuildSubjectProfileParams {
|
|
@@ -117,10 +171,14 @@ export function buildSubjectProfile(params: BuildSubjectProfileParams): SubjectP
|
|
|
117
171
|
if (systemPrompt === undefined) systemPrompt = inferSystemPrompt(traces);
|
|
118
172
|
if (given?.systemPrompt === undefined) inferredFields.push("systemPrompt");
|
|
119
173
|
|
|
174
|
+
// EV-5 — classify each tool's reversibility (GIVEN override wins; else name heuristic).
|
|
175
|
+
const toolReversibility = classifyToolList(tools, given?.toolReversibility);
|
|
176
|
+
|
|
120
177
|
const profile: SubjectProfile = {
|
|
121
178
|
identity,
|
|
122
179
|
purpose,
|
|
123
180
|
tools,
|
|
181
|
+
toolReversibility,
|
|
124
182
|
scope,
|
|
125
183
|
harness,
|
|
126
184
|
provenance: hasGiven ? SubjectProfileProvenance.Given : SubjectProfileProvenance.Reconstructed,
|
|
@@ -132,3 +190,27 @@ export function buildSubjectProfile(params: BuildSubjectProfileParams): SubjectP
|
|
|
132
190
|
};
|
|
133
191
|
return profile;
|
|
134
192
|
}
|
|
193
|
+
|
|
194
|
+
/** The compact determiner subject frame (EV-1) — a structural subset of the M1 profile. */
|
|
195
|
+
export interface SubjectFrameShape {
|
|
196
|
+
identity: string;
|
|
197
|
+
purpose: string;
|
|
198
|
+
tools: string[];
|
|
199
|
+
toolReversibility?: ToolReversibilityEntry[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Derive the determiner/judge subject frame (EV-1 / EV-5) from a subject name + trace
|
|
204
|
+
* batch. THE single derivation used by run-pipeline (Stage-B) AND prep (Stage-A) so
|
|
205
|
+
* the determiner prompt is byte-identical across the two stages (the cross-stage cache
|
|
206
|
+
* would miss on any divergence). PURE — same inputs ⇒ same frame.
|
|
207
|
+
*/
|
|
208
|
+
export function deriveSubjectFrame(subjectName: string, traces: EvalTrace[]): SubjectFrameShape {
|
|
209
|
+
const m1 = buildSubjectProfile({ subjectName, traces });
|
|
210
|
+
return {
|
|
211
|
+
identity: m1.identity,
|
|
212
|
+
purpose: m1.purpose,
|
|
213
|
+
tools: m1.tools,
|
|
214
|
+
...(m1.toolReversibility !== undefined ? { toolReversibility: m1.toolReversibility } : {}),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scripts/sync-eval-criteria.ts — the EVAL-LEG reconcile CONTRACT (Wave-2 W2I5 · KP-003).
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* The THIRD leg of the def → impl → eval triad. Today `#sync-spec` reconciles two
|
|
5
|
+
* legs — spec ↔ impl. When an implementation amends (the ⑤ OPTIMIZE loop's ai-engineer,
|
|
6
|
+
* or a brownfield drift), the eval criteria that GROUND the subject's evaluation can go
|
|
7
|
+
* stale w.r.t. the changed impl. This module is the EVALUATOR-SIDE CONTRACT that the
|
|
8
|
+
* reconcile (ai-architect REASONING, session-dispatched — Model-B) READS and WRITES:
|
|
9
|
+
*
|
|
10
|
+
* 1. WHICH eval leg applies — the subject-kind resolver. The eval leg's shape differs
|
|
11
|
+
* by subject kind (this is why W2I5 was gated behind W2I1's code-quality leg):
|
|
12
|
+
* - agent / skill / composite subject → the EVAL-SUITE criteria (the evaluator's
|
|
13
|
+
* discovered / maintained criteria for that subject).
|
|
14
|
+
* - code subject → the CODE-QUALITY criteria (W2I1's `DEFAULT_CODE_QUALITY_CRITERIA`
|
|
15
|
+
* / `#mode-judge-code-quality`).
|
|
16
|
+
* 2. A deterministic STALENESS FLAG predicate — the eval-leg half of the drift trigger
|
|
17
|
+
* (mirrors the builder freshness probe's spec/code compare; same 3-value semantics).
|
|
18
|
+
* 3. The monotonic APPLY primitive — the ai-architect reasons a criteria delta; this
|
|
19
|
+
* applies it as criteria MAINTENANCE: upsert-by-id, append the novel, and NEVER drop
|
|
20
|
+
* an existing criterion (EV-053, guarded by `assertMonotonicGrowth`).
|
|
21
|
+
*
|
|
22
|
+
* JUDGE-ONLY PRESERVED (EV-051): this file NEVER scores a subject and NEVER decides a
|
|
23
|
+
* subject pass/fail. It FLAGS freshness (pure epoch compare) and GROWS a criteria set
|
|
24
|
+
* (pure set algebra). The reconcile REASONING — which criteria a changed impl makes stale,
|
|
25
|
+
* what new criterion it needs — is the ai-architect's, not this code's (Model-B: code =
|
|
26
|
+
* drift predicates + deterministic maintenance only, never agent dispatch).
|
|
27
|
+
*
|
|
28
|
+
* PURE + deterministic (C-PIN): no clock / random / network; same inputs → byte-identical.
|
|
29
|
+
*/
|
|
30
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
31
|
+
import { Value } from "@sinclair/typebox/value";
|
|
32
|
+
|
|
33
|
+
import { assertMonotonicGrowth } from "./living-suite.ts";
|
|
34
|
+
|
|
35
|
+
// ── subject kind → eval leg ──────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/** The subject kinds the triad reconciles. `agent|skill|composite` come from the
|
|
38
|
+
* agentspec `definition.identity.kind`; `code` is the ⑤ OPTIMIZE code-target subject. */
|
|
39
|
+
export const EvalSubjectKind = {
|
|
40
|
+
Agent: "agent",
|
|
41
|
+
Skill: "skill",
|
|
42
|
+
Composite: "composite",
|
|
43
|
+
Code: "code",
|
|
44
|
+
} as const;
|
|
45
|
+
export type EvalSubjectKindValue = (typeof EvalSubjectKind)[keyof typeof EvalSubjectKind];
|
|
46
|
+
|
|
47
|
+
/** Which eval leg grounds a subject's evaluation — the W2I5 core rule. */
|
|
48
|
+
export const EvalLegKind = {
|
|
49
|
+
/** agent / skill / composite → the evaluator's discovered/maintained eval-suite criteria. */
|
|
50
|
+
EvalSuite: "eval-suite",
|
|
51
|
+
/** code → W2I1's code-quality criteria (`DEFAULT_CODE_QUALITY_CRITERIA`). */
|
|
52
|
+
CodeQuality: "code-quality",
|
|
53
|
+
} as const;
|
|
54
|
+
export type EvalLegKindValue = (typeof EvalLegKind)[keyof typeof EvalLegKind];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve WHICH eval leg a subject kind reconciles against. Code subjects use the
|
|
58
|
+
* code-quality criteria (a deterministic-test-gate PLUS a quality judge); every other
|
|
59
|
+
* kind uses the discovered eval-suite criteria. This single mapping is the shape
|
|
60
|
+
* differentiator the whole triad turns on.
|
|
61
|
+
*/
|
|
62
|
+
export function evalLegForSubjectKind(kind: EvalSubjectKindValue): EvalLegKindValue {
|
|
63
|
+
return kind === EvalSubjectKind.Code ? EvalLegKind.CodeQuality : EvalLegKind.EvalSuite;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── the staleness flag predicate (eval-leg half of the drift trigger) ────────
|
|
67
|
+
|
|
68
|
+
/** The eval-leg freshness verdict — mirrors the builder freshness probe's 3-value spec/code semantics. */
|
|
69
|
+
export const EvalLegDrift = {
|
|
70
|
+
/** no eval-criteria artifact yet — the eval leg must be CONSTRUCTED (cold). */
|
|
71
|
+
MissingEval: "missing-eval",
|
|
72
|
+
/** the eval criteria are as fresh as the impl — no reconcile write needed. */
|
|
73
|
+
InSync: "in-sync",
|
|
74
|
+
/** the impl is newer than the eval criteria — the eval leg is flagged for RECONCILE. */
|
|
75
|
+
NeedsSync: "needs-sync",
|
|
76
|
+
} as const;
|
|
77
|
+
export type EvalLegDriftValue = (typeof EvalLegDrift)[keyof typeof EvalLegDrift];
|
|
78
|
+
|
|
79
|
+
/** Effective freshness epochs (seconds) the flag predicate compares. `null` = unknown/absent. */
|
|
80
|
+
export interface EvalLegFreshnessInput {
|
|
81
|
+
/** effective freshness epoch of the eval-criteria artifact; `null` = artifact absent. */
|
|
82
|
+
evalCriteriaEpoch: number | null;
|
|
83
|
+
/** effective freshness epoch of the implementation; `null` = unknown. */
|
|
84
|
+
implEpoch: number | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* FLAG whether the eval leg is stale w.r.t. the impl. Deterministic — a pure epoch
|
|
89
|
+
* compare, identical in spirit to the builder `check-sync-spec` spec/code compare so the
|
|
90
|
+
* two legs agree on "impl is newer → reconcile". This is a FLAG only: it decides nothing
|
|
91
|
+
* about the subject and proposes no criteria — the reconcile reasoning is the agent's.
|
|
92
|
+
*/
|
|
93
|
+
export function flagEvalLegDrift(input: EvalLegFreshnessInput): EvalLegDriftValue {
|
|
94
|
+
if (input.evalCriteriaEpoch == null) return EvalLegDrift.MissingEval;
|
|
95
|
+
if (input.implEpoch != null && input.implEpoch > input.evalCriteriaEpoch) {
|
|
96
|
+
return EvalLegDrift.NeedsSync;
|
|
97
|
+
}
|
|
98
|
+
return EvalLegDrift.InSync;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The resolved eval leg for a subject: which criteria kind + the current staleness flag. */
|
|
102
|
+
export interface EvalLegAssessment {
|
|
103
|
+
leg: EvalLegKindValue;
|
|
104
|
+
drift: EvalLegDriftValue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Convenience: resolve the eval leg for a subject kind AND flag its staleness in one
|
|
109
|
+
* call — the exact pair the reconcile reads ("which leg, is it stale?").
|
|
110
|
+
*/
|
|
111
|
+
export function assessEvalLeg(
|
|
112
|
+
subjectKind: EvalSubjectKindValue,
|
|
113
|
+
freshness: EvalLegFreshnessInput,
|
|
114
|
+
): EvalLegAssessment {
|
|
115
|
+
return { leg: evalLegForSubjectKind(subjectKind), drift: flagEvalLegDrift(freshness) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── the criteria representation + the monotonic reconcile-apply ──────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* One eval criterion — subject-kind-agnostic (a stable `id` key + the binary `statement`,
|
|
122
|
+
* plus the `leg` it belongs to and an optional severity). Serves BOTH legs: a code-quality
|
|
123
|
+
* `Qn` criterion (`severity` ∈ critical|high|medium) and an eval-suite discovered criterion
|
|
124
|
+
* flow through the same shape. `additionalProperties: true` keeps a richer criterion (e.g.
|
|
125
|
+
* a code-quality `passDefinition`/`failDefinition`, an eval-suite `checkMethod`) forward-parsing.
|
|
126
|
+
*/
|
|
127
|
+
export const EvalCriterionSchema = Type.Object(
|
|
128
|
+
{
|
|
129
|
+
id: Type.String({ minLength: 1 }),
|
|
130
|
+
statement: Type.String({ minLength: 1 }),
|
|
131
|
+
leg: Type.Union([Type.Literal(EvalLegKind.EvalSuite), Type.Literal(EvalLegKind.CodeQuality)]),
|
|
132
|
+
severity: Type.Optional(Type.String({ minLength: 1 })),
|
|
133
|
+
},
|
|
134
|
+
{ additionalProperties: true },
|
|
135
|
+
);
|
|
136
|
+
export type EvalCriterion = Static<typeof EvalCriterionSchema>;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The reconcile REQUEST the ai-architect emits: the persisted criteria `existing`, plus the
|
|
140
|
+
* agent-reasoned `proposed` additions/updates. `proposed` is an UPSERT set (append the novel,
|
|
141
|
+
* revise a same-id criterion's wording for the changed impl) — it is NEVER a delete list; the
|
|
142
|
+
* monotonic-growth invariant (EV-053) forbids dropping a criterion.
|
|
143
|
+
*/
|
|
144
|
+
export const EvalCriteriaReconcileRequestSchema = Type.Object(
|
|
145
|
+
{
|
|
146
|
+
subjectId: Type.String({ minLength: 1 }),
|
|
147
|
+
subjectKind: Type.Union([
|
|
148
|
+
Type.Literal(EvalSubjectKind.Agent),
|
|
149
|
+
Type.Literal(EvalSubjectKind.Skill),
|
|
150
|
+
Type.Literal(EvalSubjectKind.Composite),
|
|
151
|
+
Type.Literal(EvalSubjectKind.Code),
|
|
152
|
+
]),
|
|
153
|
+
leg: Type.Union([Type.Literal(EvalLegKind.EvalSuite), Type.Literal(EvalLegKind.CodeQuality)]),
|
|
154
|
+
existing: Type.Array(EvalCriterionSchema),
|
|
155
|
+
proposed: Type.Array(EvalCriterionSchema),
|
|
156
|
+
},
|
|
157
|
+
{ additionalProperties: false },
|
|
158
|
+
);
|
|
159
|
+
export type EvalCriteriaReconcileRequest = Static<typeof EvalCriteriaReconcileRequestSchema>;
|
|
160
|
+
|
|
161
|
+
/** Provenance for a reconcile — pure counters (NO clock, for byte-identity / C-PIN). */
|
|
162
|
+
export interface EvalCriteriaReconcileProvenance {
|
|
163
|
+
/** how many genuinely-novel criteria were appended. */
|
|
164
|
+
added: number;
|
|
165
|
+
/** how many existing criteria (same id) had their content revised. */
|
|
166
|
+
updated: number;
|
|
167
|
+
/** total criteria after the reconcile. */
|
|
168
|
+
total: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The reconcile RESULT — the grown/maintained criteria set + provenance. */
|
|
172
|
+
export interface EvalCriteriaReconcileResult {
|
|
173
|
+
subjectId: string;
|
|
174
|
+
leg: EvalLegKindValue;
|
|
175
|
+
criteria: EvalCriterion[];
|
|
176
|
+
provenance: EvalCriteriaReconcileProvenance;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Parse + narrow a reconcile request (guarded). THROWS on schema violation. PURE. */
|
|
180
|
+
export function parseEvalCriteriaReconcileRequest(value: unknown): EvalCriteriaReconcileRequest {
|
|
181
|
+
if (!Value.Check(EvalCriteriaReconcileRequestSchema, value)) {
|
|
182
|
+
const first = [...Value.Errors(EvalCriteriaReconcileRequestSchema, value)][0];
|
|
183
|
+
throw new Error(
|
|
184
|
+
`parseEvalCriteriaReconcileRequest: schema violation at '${first?.path ?? "(root)"}': ` +
|
|
185
|
+
`${first?.message ?? "invalid eval-criteria reconcile request"}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const criterionId = (c: EvalCriterion): string => c.id;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* APPLY the reconcile: maintain the eval-criteria set for a changed impl. Criteria
|
|
195
|
+
* MAINTENANCE, not judging (EV-051): upsert each `proposed` criterion by `id` (revise a
|
|
196
|
+
* same-id criterion's content in place, keeping the id), append the genuinely-novel, and
|
|
197
|
+
* assert the id-set NEVER shrank (EV-053, via the shared `assertMonotonicGrowth`). The
|
|
198
|
+
* `leg` on every proposed criterion must match the request's `leg` — mixing a code-quality
|
|
199
|
+
* criterion into an eval-suite reconcile (or vice versa) is a hard error, not a silent
|
|
200
|
+
* accept. DETERMINISTIC + PURE: existing order preserved, novel appended in proposal order;
|
|
201
|
+
* no clock / random. Same (existing, proposed) → byte-identical result.
|
|
202
|
+
*/
|
|
203
|
+
export function reconcileEvalCriteria(
|
|
204
|
+
request: EvalCriteriaReconcileRequest,
|
|
205
|
+
): EvalCriteriaReconcileResult {
|
|
206
|
+
const req = parseEvalCriteriaReconcileRequest(request);
|
|
207
|
+
|
|
208
|
+
for (const p of req.proposed) {
|
|
209
|
+
if (p.leg !== req.leg) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`reconcileEvalCriteria: proposed criterion '${p.id}' has leg '${p.leg}' but the ` +
|
|
212
|
+
`reconcile targets leg '${req.leg}'. An eval-suite and a code-quality criterion ` +
|
|
213
|
+
"are different legs — refusing to mix them (subject-kind integrity).",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Insertion-ordered upsert: existing order first (content possibly revised), novel appended.
|
|
219
|
+
const byId = new Map<string, EvalCriterion>();
|
|
220
|
+
for (const c of req.existing) byId.set(c.id, c);
|
|
221
|
+
|
|
222
|
+
let added = 0;
|
|
223
|
+
let updated = 0;
|
|
224
|
+
for (const p of req.proposed) {
|
|
225
|
+
if (byId.has(p.id)) {
|
|
226
|
+
byId.set(p.id, p); // revise content, id (and thus the key/order) retained
|
|
227
|
+
updated += 1;
|
|
228
|
+
} else {
|
|
229
|
+
byId.set(p.id, p); // novel → appended after the existing keys
|
|
230
|
+
added += 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const criteria = [...byId.values()];
|
|
235
|
+
// EV-053: a maintained criteria set may GROW or revise, but never DROP a criterion.
|
|
236
|
+
assertMonotonicGrowth(req.existing, criteria, criterionId);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
subjectId: req.subjectId,
|
|
240
|
+
leg: req.leg,
|
|
241
|
+
criteria,
|
|
242
|
+
provenance: { added, updated, total: criteria.length },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -164,6 +164,34 @@ function effectiveRole(span: UnitfSpan): UnitfRole {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// ── Message-text pick (INF-1) ────────────────────────────────────────────────
|
|
168
|
+
//
|
|
169
|
+
// ROOT CAUSE of the Claude Code loss: the Claude Code exporter writes BOTH user
|
|
170
|
+
// and assistant TEXT into `span.output` (a user turn is `kind:"event", role:"user",
|
|
171
|
+
// output:<text>`; an assistant turn is `kind:"llm", role:"assistant", output:<text>`
|
|
172
|
+
// with `input` UNSET). The old projection read a user/system span's `.input` — empty
|
|
173
|
+
// for EVERY Claude Code trace ⇒ `input.prompt` silently empty for all 21/21.
|
|
174
|
+
//
|
|
175
|
+
// This VENDORS the shared reader's role-conditional pick + fallback
|
|
176
|
+
// (@mutagent/tools `messagesView`, derive.ts:173-174): a user/system turn's text is
|
|
177
|
+
// canonically in `input`, everything else in `output`, but the `?? input ?? output`
|
|
178
|
+
// fallback recovers the text WHICHEVER field a producer used. Vendored — never
|
|
179
|
+
// imported: the standalone-publish guard forbids the evaluator reaching into
|
|
180
|
+
// @mutagent/tools (MIGRATION-diagnostics-evaluator.md:417); diagnostics vendors its
|
|
181
|
+
// own copy and this mirrors that.
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The span's effective message TEXT: the role-conditional field pick with the
|
|
185
|
+
* `?? input ?? output` fallback (mirrors @mutagent/tools `messagesView`). Recovers
|
|
186
|
+
* a Claude Code turn (text in `output`) AND a Langfuse turn (text in `input`).
|
|
187
|
+
* PURE; "" when the span carries no text.
|
|
188
|
+
*/
|
|
189
|
+
function messageText(span: UnitfSpan): string {
|
|
190
|
+
const role = effectiveRole(span);
|
|
191
|
+
const primary = role === "user" || role === "system" ? span.input : span.output;
|
|
192
|
+
return asText(primary ?? span.input ?? span.output);
|
|
193
|
+
}
|
|
194
|
+
|
|
167
195
|
// ── Token total fallback (XF-FIX Finding D) ──────────────────────────────────
|
|
168
196
|
//
|
|
169
197
|
// MIRRORS diagnostics `unitf-adapter.ts` `fallbackTotalTokens` so both lanes
|
|
@@ -185,10 +213,11 @@ function fallbackTotalTokens(t: UnitfTokens | undefined): number | undefined {
|
|
|
185
213
|
*
|
|
186
214
|
* id ← ut.traceId
|
|
187
215
|
* name ← ut.agentName
|
|
188
|
-
* output ←
|
|
189
|
-
* (XF-FIX A: role-less GENERATION spans read as assistant via kind
|
|
190
|
-
*
|
|
191
|
-
*
|
|
216
|
+
* output ← LAST EFFECTIVE-assistant llm span WITH text → { response: <messageText> }
|
|
217
|
+
* (XF-FIX A: role-less GENERATION spans read as assistant via kind;
|
|
218
|
+
* INF-1: last-with-text, and messageText recovers Claude Code's `output`)
|
|
219
|
+
* input ← first user|system span's messageText, ELSE the response span's own
|
|
220
|
+
* input → { prompt } (XF-FIX A: Langfuse prompt; INF-1: Claude Code `output`)
|
|
192
221
|
* observations ← ut.spans → { type: kind.toUpperCase(), name, input, output }
|
|
193
222
|
* errored/status ← computed.hasError ?? (status==="error" || any span error) / ut.status (XF-FIX C)
|
|
194
223
|
* tokens/totalTokens← ut.tokens split / computed.totalTokens ?? tokens.total ?? in+out (XF-FIX D)
|
|
@@ -216,31 +245,45 @@ export function projectUnitfToEvalTrace(ut: UnifiedTraceLike): EvalTrace {
|
|
|
216
245
|
|
|
217
246
|
if (ut.agentName !== undefined) out.name = ut.agentName;
|
|
218
247
|
|
|
219
|
-
// output ←
|
|
220
|
-
// assistant role is
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
248
|
+
// output ← the LAST assistant LLM span WITH text, wrapped to { response }. The
|
|
249
|
+
// assistant role is EFFECTIVE (XF-FIX Finding A): a role-less GENERATION span
|
|
250
|
+
// (kind==="llm", the Langfuse shape) reads as assistant via `effectiveRole`. The
|
|
251
|
+
// second INF-1 miss: the old code took the FIRST assistant span — on Claude Code
|
|
252
|
+
// that is usually a tool_use turn with EMPTY output text, so the real reply (a
|
|
253
|
+
// LATER assistant span) was dropped. Take the LAST assistant span that carries
|
|
254
|
+
// text; the `messageText` pick recovers it from `output` (Claude Code) or `input`
|
|
255
|
+
// (whichever field the producer used).
|
|
256
|
+
const assistantSpans = ut.spans.filter(
|
|
224
257
|
(s) => effectiveRole(s) === "assistant" && s.kind === "llm",
|
|
225
258
|
);
|
|
226
|
-
|
|
227
|
-
|
|
259
|
+
let responseSpan: UnitfSpan | undefined;
|
|
260
|
+
for (const s of assistantSpans) {
|
|
261
|
+
if (messageText(s).length > 0) responseSpan = s; // keep the LAST with text
|
|
262
|
+
}
|
|
263
|
+
// Fall back to the last assistant span even without text, so the response SLOT is
|
|
264
|
+
// still derived from a real span (the empty case is WARNED at intake, not here).
|
|
265
|
+
const lastAssistant =
|
|
266
|
+
responseSpan ?? (assistantSpans.length > 0 ? assistantSpans[assistantSpans.length - 1] : undefined);
|
|
267
|
+
if (responseSpan !== undefined) {
|
|
268
|
+
out.output = { response: messageText(responseSpan) };
|
|
228
269
|
}
|
|
229
270
|
|
|
230
|
-
// input ← first user|system span's
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// response
|
|
235
|
-
//
|
|
271
|
+
// input ← the first user|system span's TEXT, wrapped to { prompt }. `messageText`
|
|
272
|
+
// recovers the text from `output` (Claude Code writes the user turn there) or
|
|
273
|
+
// `input` (Langfuse). When there is no explicit user/system turn (the Langfuse
|
|
274
|
+
// shape — one GENERATION span carrying the prompt in its OWN `input` and the
|
|
275
|
+
// response in `output`) the prompt is recovered from the response span's `input`
|
|
276
|
+
// SPECIFICALLY (not `messageText`, which for an assistant span would return the
|
|
277
|
+
// response). Without these fallbacks `input.prompt` was empty for every Claude
|
|
278
|
+
// Code trace (INF-1) and every Langfuse trace (Finding A).
|
|
236
279
|
const promptSpan = ut.spans.find(
|
|
237
280
|
(s) => effectiveRole(s) === "user" || effectiveRole(s) === "system",
|
|
238
281
|
);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
out.input = { prompt: asText(responseSpan.input) };
|
|
282
|
+
let promptText = promptSpan !== undefined ? messageText(promptSpan) : "";
|
|
283
|
+
if (promptText.length === 0 && lastAssistant !== undefined && lastAssistant.input !== undefined) {
|
|
284
|
+
promptText = asText(lastAssistant.input);
|
|
243
285
|
}
|
|
286
|
+
if (promptText.length > 0) out.input = { prompt: promptText };
|
|
244
287
|
|
|
245
288
|
// Finding C — carry structured error/status so an error-KIND criterion has a
|
|
246
289
|
// field to read (previously error state survived only as textual span output).
|