engineering-behavior-observatory 0.2.1
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/LICENSE +202 -0
- package/README.md +151 -0
- package/contracts/codex-app-server-0.150.1/manifest.json +25 -0
- package/contracts/codex-app-server-0.150.1/schema/InitializeParams.json +84 -0
- package/contracts/codex-app-server-0.150.1/schema/JSONRPCMessage.json +137 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/ThreadReadParams.json +17 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
- package/contracts/codex-app-server-0.150.1/schema/v2/TurnInterruptParams.json +17 -0
- package/contracts/codex-app-server-0.150.1/types/AskForApproval.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/ClientNotification.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/SandboxMode.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/ThreadReadParams.ts +9 -0
- package/contracts/codex-app-server-0.150.1/types/TokenUsageBreakdown.ts +5 -0
- package/contracts/codex-app-server-0.150.1/types/TurnInterruptParams.ts +5 -0
- package/contracts/codex-app-server-0.153.4/manifest.json +27 -0
- package/contracts/codex-app-server-0.153.4/schema/JSONRPCMessage.json +137 -0
- package/contracts/codex-app-server-0.153.4/schema/v1/InitializeParams.json +84 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadReadParams.json +17 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadStartParams.json +515 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/ThreadTokenUsageUpdatedNotification.json +82 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/TurnInterruptParams.json +17 -0
- package/contracts/codex-app-server-0.153.4/schema/v2/TurnStartParams.json +921 -0
- package/contracts/codex-app-server-0.153.4/types/AskForApproval.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/ClientNotification.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/SandboxMode.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/ThreadReadParams.ts +12 -0
- package/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.ts +5 -0
- package/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.ts +5 -0
- package/contracts/openhands-agent-server-v1.44.1.json +65 -0
- package/contracts/openhands-agent-server-v1.46.0.json +46 -0
- package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.d.ts +9 -0
- package/dist/contracts/codex-app-server-0.153.4/types/AskForApproval.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.d.ts +3 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ClientNotification.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.d.ts +1 -0
- package/dist/contracts/codex-app-server-0.153.4/types/SandboxMode.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.d.ts +10 -0
- package/dist/contracts/codex-app-server-0.153.4/types/ThreadReadParams.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.d.ts +8 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TokenUsageBreakdown.js +2 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.d.ts +4 -0
- package/dist/contracts/codex-app-server-0.153.4/types/TurnInterruptParams.js +2 -0
- package/dist/src/agent-sdk-normalizer.d.ts +22 -0
- package/dist/src/agent-sdk-normalizer.js +995 -0
- package/dist/src/agent-sdk-run.d.ts +31 -0
- package/dist/src/agent-sdk-run.js +242 -0
- package/dist/src/agent-sdk-runner.d.ts +96 -0
- package/dist/src/agent-sdk-runner.js +407 -0
- package/dist/src/agent-sdk.d.ts +294 -0
- package/dist/src/agent-sdk.js +644 -0
- package/dist/src/aggregation.d.ts +183 -0
- package/dist/src/aggregation.js +513 -0
- package/dist/src/artifacts.d.ts +36 -0
- package/dist/src/artifacts.js +1372 -0
- package/dist/src/atlas-grafana.d.ts +377 -0
- package/dist/src/atlas-grafana.js +47 -0
- package/dist/src/atlas-html.d.ts +2 -0
- package/dist/src/atlas-html.js +34 -0
- package/dist/src/atlas.d.ts +130 -0
- package/dist/src/atlas.js +235 -0
- package/dist/src/behavior-assertions.d.ts +95 -0
- package/dist/src/behavior-assertions.js +154 -0
- package/dist/src/cli.d.ts +5 -0
- package/dist/src/cli.js +957 -0
- package/dist/src/codex-judge.d.ts +44 -0
- package/dist/src/codex-judge.js +214 -0
- package/dist/src/codex-run.d.ts +114 -0
- package/dist/src/codex-run.js +519 -0
- package/dist/src/codex.d.ts +187 -0
- package/dist/src/codex.js +1028 -0
- package/dist/src/contracts.d.ts +155 -0
- package/dist/src/contracts.js +866 -0
- package/dist/src/corpus.d.ts +67 -0
- package/dist/src/corpus.js +540 -0
- package/dist/src/cursor-sdk-runner.d.ts +81 -0
- package/dist/src/cursor-sdk-runner.js +362 -0
- package/dist/src/cursor-sdk.d.ts +81 -0
- package/dist/src/cursor-sdk.js +1078 -0
- package/dist/src/deepseek-adapter.d.ts +152 -0
- package/dist/src/deepseek-adapter.js +777 -0
- package/dist/src/exports.d.ts +69 -0
- package/dist/src/exports.js +966 -0
- package/dist/src/human-calibration.d.ts +167 -0
- package/dist/src/human-calibration.js +618 -0
- package/dist/src/index.d.ts +71 -0
- package/dist/src/index.js +38 -0
- package/dist/src/lifecycle.d.ts +196 -0
- package/dist/src/lifecycle.js +1889 -0
- package/dist/src/normalization-integrity.d.ts +128 -0
- package/dist/src/normalization-integrity.js +429 -0
- package/dist/src/openhands-run.d.ts +36 -0
- package/dist/src/openhands-run.js +415 -0
- package/dist/src/openhands.d.ts +130 -0
- package/dist/src/openhands.js +859 -0
- package/dist/src/pi.d.ts +203 -0
- package/dist/src/pi.js +1345 -0
- package/dist/src/process-protocol.d.ts +245 -0
- package/dist/src/process-protocol.js +1261 -0
- package/dist/src/retained-evidence.d.ts +12 -0
- package/dist/src/retained-evidence.js +201 -0
- package/dist/src/run-bundles.d.ts +193 -0
- package/dist/src/run-bundles.js +993 -0
- package/dist/src/scheduler.d.ts +109 -0
- package/dist/src/scheduler.js +1080 -0
- package/dist/src/semantic-judge.d.ts +194 -0
- package/dist/src/semantic-judge.js +875 -0
- package/dist/src/structural-observations.d.ts +124 -0
- package/dist/src/structural-observations.js +671 -0
- package/dist/src/task-packets.d.ts +115 -0
- package/dist/src/task-packets.js +683 -0
- package/dist/src/uniform-events.d.ts +120 -0
- package/dist/src/uniform-events.js +158 -0
- package/dist/src/verifiers.d.ts +116 -0
- package/dist/src/verifiers.js +819 -0
- package/dist/src/workspaces.d.ts +56 -0
- package/dist/src/workspaces.js +1314 -0
- package/docs/README.md +60 -0
- package/docs/development/README.md +71 -0
- package/docs/development/documentation-sync.md +58 -0
- package/docs/development/extension-contracts.md +307 -0
- package/docs/evaluation/README.md +48 -0
- package/docs/evaluation/aggregation.md +112 -0
- package/docs/evaluation/behavior-assertions.md +45 -0
- package/docs/evaluation/human-calibration.md +170 -0
- package/docs/evaluation/normalization-integrity.md +80 -0
- package/docs/evaluation/semantic-judge.md +188 -0
- package/docs/evaluation/structural-observations.md +84 -0
- package/docs/evaluation/uniform-events.md +131 -0
- package/docs/guides/atlas.md +144 -0
- package/docs/guides/evidence-and-sharing.md +100 -0
- package/docs/guides/operator-guide.md +424 -0
- package/docs/guides/quickstart.md +152 -0
- package/docs/guides/telemetry.md +66 -0
- package/docs/harnesses/README.md +31 -0
- package/docs/harnesses/claude-agent-sdk.md +61 -0
- package/docs/harnesses/codex-harness.md +174 -0
- package/docs/harnesses/cursor-sdk.md +149 -0
- package/docs/harnesses/deepseek-harness.md +134 -0
- package/docs/harnesses/openhands-agent-server.md +153 -0
- package/docs/harnesses/pi-sdk.md +66 -0
- package/docs/reference/README.md +19 -0
- package/docs/reference/agent-sdk-operational-runner.md +118 -0
- package/docs/reference/cli.md +114 -0
- package/docs/reference/contracts.md +222 -0
- package/docs/reference/run-bundle-contract.md +354 -0
- package/docs/reference/run-lifecycle.md +54 -0
- package/examples/cursor-sdk/README.md +11 -0
- package/examples/cursor-sdk/capture-profile.json +1 -0
- package/examples/cursor-sdk/harness.json +1 -0
- package/examples/cursor-sdk/model.json +1 -0
- package/examples/cursor-sdk/native-limits.json +1 -0
- package/examples/cursor-sdk/native-tool-policy.json +1 -0
- package/ontology/behavior-categories.v1.json +46 -0
- package/package.json +66 -0
- package/release/0.1.0/KNOWN_LIMITATIONS.md +35 -0
- package/release/0.1.0/README.md +46 -0
- package/release/0.1.0/reproducibility.json +61 -0
- package/release/0.2.0/KNOWN_LIMITATIONS.md +40 -0
- package/release/0.2.0/README.md +42 -0
- package/release/0.2.0/reproducibility.json +73 -0
- package/release/0.2.1/KNOWN_LIMITATIONS.md +18 -0
- package/release/0.2.1/README.md +63 -0
- package/release/0.2.1/reproducibility.json +73 -0
- package/release/README.md +13 -0
- package/schemas/aggregation.v1.json +293 -0
- package/schemas/behavior-assertions.v1.json +220 -0
- package/schemas/deepseek-runtime-composition.v1.schema.json +122 -0
- package/schemas/experiment.v1.schema.json +188 -0
- package/schemas/human-calibration.v1.json +336 -0
- package/schemas/normalization-integrity.v1.json +302 -0
- package/schemas/run-bundles/v1.json +1040 -0
- package/schemas/run-queue.v1.schema.json +206 -0
- package/schemas/semantic-judge.v1.json +380 -0
- package/schemas/structural-observations.v1.json +131 -0
- package/schemas/task-packet-freeze.v1.schema.json +106 -0
- package/schemas/task-packet.v1.schema.json +234 -0
- package/schemas/uniform-events/v1.json +289 -0
- package/scripts/atlas-grafana.sh +18 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { aggregateEvaluation, selectAggregationAttempts } from "./aggregation.js";
|
|
7
|
+
import { assertNoDuplicateJsonKeys, canonicalizeMetadata, digestMetadata, validateArtifact } from "./artifacts.js";
|
|
8
|
+
import { createRetainedBehaviorEvidence } from "./retained-evidence.js";
|
|
9
|
+
import { readCorpusIndex, validateCorpusIndex } from "./corpus.js";
|
|
10
|
+
import { readPortableRunBundleExport, sanitizeDerivedExport } from "./exports.js";
|
|
11
|
+
import { assertCalibrationDestination, effectiveReviewOutcome, isDisputedReviewOutcome } from "./human-calibration.js";
|
|
12
|
+
import { readBoundedFile } from "./scheduler.js";
|
|
13
|
+
import { renderAtlas } from "./atlas-html.js";
|
|
14
|
+
import { writeAtlasGrafana } from "./atlas-grafana.js";
|
|
15
|
+
export const ATLAS_FILTERS = ["model", "harness", "task", "trial", "capture", "outcome", "review", "category", "assessment", "q"];
|
|
16
|
+
export function atlasBehaviorPartitions(view) {
|
|
17
|
+
return view.report.groups.flatMap((group) => (group.behaviors ?? []).map((partition) => ({ group: Object.values(group.dimensions).join(" · ") || "Selected cohort", partition })));
|
|
18
|
+
}
|
|
19
|
+
export function atlasBehaviorRows(view) {
|
|
20
|
+
const rows = atlasBehaviorPartitions(view).flatMap(({ group, partition }) => partition.assessments.map(({ assessment, measurement }) => ({ group, category: partition.behavior.categoryId, dimension: partition.behavior.dimensionId, vocabulary: partition.behavior.vocabularyVersion, rubric: `${partition.rubric.id}@${partition.rubric.version}`, evaluator: canonicalizeMetadata(partition.evaluator), assessment, label: `${group} · ${partition.behavior.categoryId} · ${assessment}`, status: measurement.status, numerator: measurement.status === "available" ? measurement.numerator.value : null, denominator: measurement.denominator.value, unit: measurement.denominator.unit, exclusions: measurement.exclusions.map(({ reason, count }) => `${reason}: ${count}`).join("; "), cohortDigest: view.cohortDigest })));
|
|
21
|
+
return rows.length ? rows : [{ group: "Selected cohort", category: "unavailable", dimension: "unavailable", vocabulary: "unavailable", rubric: "unavailable", evaluator: "unavailable", assessment: "unavailable", label: "Behavior distribution unavailable", status: "unavailable", numerator: null, denominator: 0, unit: "confirmed-attempt-dimension", exclusions: "No behavior assessment partition is available.", cohortDigest: view.cohortDigest }];
|
|
22
|
+
}
|
|
23
|
+
const digest = (value) => `sha256:${digestMetadata(value).value}`;
|
|
24
|
+
const attemptKey = (value) => `${value.runId}\0${value.attemptId}`;
|
|
25
|
+
export async function loadAtlas(requestPath) {
|
|
26
|
+
const request = readJson(requestPath);
|
|
27
|
+
exactKeys(request, ["schemaVersion", "aggregationRequest", "title", "operatorNarrative", "reviewPackets", "traces", "grafanaUrl", "atlasUrl", "tempoDatasourceUid", "sharing"]);
|
|
28
|
+
if (request.schemaVersion !== "ebo.atlas-request/v1" || typeof request.title !== "string" || !request.title.trim() || typeof request.aggregationRequest !== "string")
|
|
29
|
+
throw new Error("Invalid or incompatible Atlas request.");
|
|
30
|
+
if (request.operatorNarrative !== undefined && typeof request.operatorNarrative !== "string")
|
|
31
|
+
throw new Error("Operator narrative must be text.");
|
|
32
|
+
if (request.grafanaUrl !== undefined)
|
|
33
|
+
localUrl(request.grafanaUrl);
|
|
34
|
+
if (request.atlasUrl !== undefined)
|
|
35
|
+
localUrl(request.atlasUrl);
|
|
36
|
+
const traceKeys = new Set();
|
|
37
|
+
for (const trace of request.traces ?? []) {
|
|
38
|
+
if (typeof trace.runId !== "string" || !trace.runId || typeof trace.attemptId !== "string" || !trace.attemptId
|
|
39
|
+
|| !/^[a-f0-9]{32}$/u.test(trace.traceId) || !Number.isFinite(Date.parse(trace.originalStart))
|
|
40
|
+
|| (trace.replayStart !== undefined && !Number.isFinite(Date.parse(trace.replayStart))))
|
|
41
|
+
throw new Error("Invalid trace identity or timestamp.");
|
|
42
|
+
const key = attemptKey(trace);
|
|
43
|
+
if (traceKeys.has(key))
|
|
44
|
+
throw new Error("Atlas trace attempt keys must be unique; conflicting trace bindings are not allowed.");
|
|
45
|
+
traceKeys.add(key);
|
|
46
|
+
}
|
|
47
|
+
const base = dirname(resolve(requestPath));
|
|
48
|
+
for (const path of request.reviewPackets ?? []) {
|
|
49
|
+
if (typeof path !== "string" || !statSync(resolve(base, path), { throwIfNoEntry: false })?.isFile())
|
|
50
|
+
throw new Error("Configured human review packet is unavailable.");
|
|
51
|
+
}
|
|
52
|
+
const aggregationPath = resolve(base, request.aggregationRequest);
|
|
53
|
+
const aggregation = readArtifact(aggregationPath);
|
|
54
|
+
const aggregateBase = dirname(aggregationPath);
|
|
55
|
+
const source = aggregation.sources;
|
|
56
|
+
const corpusRoot = resolve(aggregateBase, source.corpusRoot);
|
|
57
|
+
const corpusEntries = readCorpusIndex(resolve(aggregateBase, source.corpusIndex));
|
|
58
|
+
const issues = validateCorpusIndex(corpusRoot, corpusEntries);
|
|
59
|
+
if (issues.length)
|
|
60
|
+
throw new Error("Atlas corpus index is stale or invalid; rebuild and validate it before querying.");
|
|
61
|
+
const input = {
|
|
62
|
+
lineage: { requestDigest: digest(aggregation), corpusIndexDigest: digest(corpusEntries) }, corpusEntries,
|
|
63
|
+
observationSets: source.observationSets.map(({ bundleRoot, path }) => ({ bundleRoot: resolve(aggregateBase, bundleRoot), document: readArtifact(resolve(aggregateBase, path)) })),
|
|
64
|
+
assertions: source.assertions.map(({ bundleRoot, path }) => ({ bundleRoot: resolve(aggregateBase, bundleRoot), document: readArtifact(resolve(aggregateBase, path)) })),
|
|
65
|
+
calibrations: source.calibrations.map(({ selection, history }) => ({ selection: readArtifact(resolve(aggregateBase, selection)), history: readArtifact(resolve(aggregateBase, history)) })),
|
|
66
|
+
comparisons: aggregation.comparisons.map(({ eligibilityGates, ...comparison }) => ({ ...comparison, eligibility: eligibilityGates.map(({ request, report }) => ({ request: readArtifact(resolve(aggregateBase, request)), report: readArtifact(resolve(aggregateBase, report)) })) })),
|
|
67
|
+
};
|
|
68
|
+
// Validate the complete source before any case or review state is exposed.
|
|
69
|
+
const validated = await aggregateEvaluation(input, aggregation);
|
|
70
|
+
const cases = [];
|
|
71
|
+
const seenAssertions = new Set();
|
|
72
|
+
for (const entry of corpusEntries.filter(({ manifestKind }) => manifestKind === "run")) {
|
|
73
|
+
const ownAssertions = input.assertions.filter(({ document }) => attemptKey(document) === attemptKey(entry));
|
|
74
|
+
const context = { runId: entry.runId, attemptId: entry.attemptId, model: entry.modelId ?? "unavailable", harness: entry.harnessId ?? "unavailable", harnessVersion: entry.harnessVersion ?? "unavailable", task: entry.taskId ?? "unavailable", trial: entry.trialId ?? "unavailable", capture: entry.captureQualification ?? "unavailable", outcome: entry.terminalState ?? "unavailable", ...(entry.retryOf ? { retryOf: entry.retryOf } : {}) };
|
|
75
|
+
let trace;
|
|
76
|
+
const traceSource = request.traces?.find((item) => attemptKey(item) === attemptKey(entry));
|
|
77
|
+
if (traceSource) {
|
|
78
|
+
if (request.grafanaUrl && request.tempoDatasourceUid) {
|
|
79
|
+
const panes = { atlas: { datasource: request.tempoDatasourceUid, queries: [{ refId: "A", queryType: "traceId", query: traceSource.traceId }], range: { from: "now-1h", to: "now" } } };
|
|
80
|
+
trace = { href: `${localUrl(request.grafanaUrl)}/explore?schemaVersion=1&panes=${encodeURIComponent(JSON.stringify(panes))}`, originalStart: traceSource.originalStart, ...(traceSource.replayStart ? { replayStart: traceSource.replayStart } : {}) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!ownAssertions.length)
|
|
84
|
+
cases.push({ ...context, key: digest(context).slice(7), category: "unavailable", assessment: "unavailable", review: "unavailable", decisions: [], citations: [], ...(trace ? { trace } : {}) });
|
|
85
|
+
for (const { document: assertion, bundleRoot } of ownAssertions) {
|
|
86
|
+
const assertionDigest = digest(assertion);
|
|
87
|
+
if (seenAssertions.has(assertionDigest))
|
|
88
|
+
continue;
|
|
89
|
+
seenAssertions.add(assertionDigest);
|
|
90
|
+
const reviews = input.calibrations.flatMap(({ selection, history }) => selection.candidates.filter(({ assertion: binding }) => binding.id === assertion.id && binding.digest === assertionDigest).map((candidate) => ({ candidate, history })));
|
|
91
|
+
const latest = reviews[0];
|
|
92
|
+
const outcome = latest ? effectiveReviewOutcome(latest.candidate, latest.history.decisions) : assertion.judgment.disposition === "abstained" ? "judge-abstained" : "unreviewed";
|
|
93
|
+
const review = latest && isDisputedReviewOutcome(latest.candidate, latest.history.decisions) ? "disputed" : { "judge-abstained": "abstained", unreviewed: "proposed", unresolved: "insufficient-evidence", confirmed: "confirmed", rejected: "rejected" }[outcome];
|
|
94
|
+
const evidence = await createRetainedBehaviorEvidence(bundleRoot);
|
|
95
|
+
const citations = assertion.judgment.citations.map((citation) => {
|
|
96
|
+
const normalizedEvent = evidence.dataset.events.find(({ id }) => id === citation.eventId);
|
|
97
|
+
const native = evidence.capture.records.find(({ reference }) => canonicalizeMetadata(reference) === canonicalizeMetadata(citation.nativeReference));
|
|
98
|
+
if (!normalizedEvent || !native)
|
|
99
|
+
throw new Error("Atlas citation cannot resolve to normalized and native evidence.");
|
|
100
|
+
return { ...citation, normalizedEvent: displaySafe(normalizedEvent), nativeRecord: displaySafe(evidence.dataset.adapter.harness === "claude-agent-sdk" ? native.record.document : native.record) };
|
|
101
|
+
});
|
|
102
|
+
const decisions = [...new Map(reviews.flatMap(({ history }) => history.decisions.filter(({ assertion: binding }) => binding.id === assertion.id && binding.digest === assertionDigest)).map((decision) => [digest(decision), decision])).values()];
|
|
103
|
+
cases.push({ ...context, key: assertionDigest.slice(7), assertion: displaySafe(assertion), assertionDigest, category: assertion.behavior.categoryId, assessment: assertion.judgment.disposition === "assessed" ? assertion.judgment.assessment : "abstained", review, decisions: displaySafe(decisions), citations, ...(trace ? { trace } : {}) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { request, aggregation, input, corpusRoot, requestPath: resolve(requestPath), cases, sourceDigest: digest({ request, lineage: validated.sourceLineage }) };
|
|
107
|
+
}
|
|
108
|
+
export async function queryAtlas(source, filters = {}) {
|
|
109
|
+
exactKeys(filters, [...ATLAS_FILTERS]);
|
|
110
|
+
for (const value of Object.values(filters))
|
|
111
|
+
if (typeof value !== "string" || value.length > 500)
|
|
112
|
+
throw new Error("Invalid Atlas filter.");
|
|
113
|
+
const eligible = selectAggregationAttempts(source.input.corpusEntries, source.aggregation.selectedAttemptPolicy);
|
|
114
|
+
const eligibleIds = new Set(eligible.map(attemptKey));
|
|
115
|
+
const eligibleCases = source.cases.filter((item) => eligibleIds.has(attemptKey(item)));
|
|
116
|
+
const matches = eligibleCases.filter((item) => Object.entries(filters).every(([key, value]) => !value || (key === "q" ? [item.runId, item.attemptId, item.model, item.task, item.assertion?.judgment.rationale ?? "", item.category].join(" ").toLocaleLowerCase().includes(value.toLocaleLowerCase()) : item[key] === value)));
|
|
117
|
+
// Filters select attempts through matching cases; all judgments on those attempts
|
|
118
|
+
// remain in aggregation so a constructive filter cannot erase an adverse rerun.
|
|
119
|
+
const ids = new Set(matches.map(attemptKey));
|
|
120
|
+
const corpusEntries = source.input.corpusEntries.filter((entry) => ids.has(attemptKey(entry)));
|
|
121
|
+
const report = await aggregateEvaluation({ ...source.input, lineage: { ...source.input.lineage, corpusIndexDigest: digest(corpusEntries) }, corpusEntries, observationSets: source.input.observationSets.filter(({ document }) => ids.has(attemptKey(document))), assertions: source.input.assertions.filter(({ document }) => ids.has(attemptKey(document))) }, source.aggregation);
|
|
122
|
+
const filterOptions = Object.fromEntries(ATLAS_FILTERS.filter((key) => key !== "q").map((key) => [key, [...new Set(eligibleCases.map((item) => String(item[key])))].sort()]));
|
|
123
|
+
const sourceAttempts = new Set(source.input.corpusEntries.filter(({ manifestKind }) => manifestKind === "run").map(attemptKey)).size;
|
|
124
|
+
return { schemaVersion: "ebo.atlas-view/v1", title: source.request.title, atlasUrl: localUrl(source.request.atlasUrl ?? "http://127.0.0.1:13011"), mode: "restricted-local-only", generatedAt: new Date().toISOString(), sourceDigest: source.sourceDigest, cohortDigest: digest({ source: source.sourceDigest, filters, lineage: report.sourceLineage }), filters, filterOptions, report, cases: matches, matchingCases: matches.length, sourceAttempts, policyExcludedAttempts: sourceAttempts - eligible.length, filteredOutAttempts: eligible.length - ids.size, ...(source.request.operatorNarrative ? { operatorNarrative: displaySafe({ text: source.request.operatorNarrative }).text } : {}), reviewPackets: (source.request.reviewPackets ?? []).map((path) => pathToFileURL(resolve(dirname(source.requestPath), path)).href), ...(source.request.grafanaUrl ? { grafanaUrl: localUrl(source.request.grafanaUrl) } : {}) };
|
|
125
|
+
}
|
|
126
|
+
export async function shareAtlas(source, view) {
|
|
127
|
+
const sharing = source.request.sharing;
|
|
128
|
+
if (!sharing)
|
|
129
|
+
throw new Error("Sharing unavailable: configure approved exports and explicit report fields first.");
|
|
130
|
+
if (["review", "category", "assessment", "q"].some((key) => view.filters[key]))
|
|
131
|
+
throw new Error("Shareable summaries cannot use semantic, human-review, or free-text evidence filters; their cohort counts would disclose unapproved findings.");
|
|
132
|
+
exactKeys(sharing, ["policy", "approvedExports", "fields"]);
|
|
133
|
+
const supported = ["cohort", "aggregate-metrics", "source-digests"];
|
|
134
|
+
if (sharing.fields.length !== supported.length || !supported.every((field) => sharing.fields.includes(field)))
|
|
135
|
+
throw new Error("Unsupported report-field sharing; only cohort, aggregate-metrics and source-digests are supported.");
|
|
136
|
+
const approved = await Promise.all(sharing.approvedExports.map((root) => readPortableRunBundleExport(resolve(dirname(source.requestPath), root), sharing.policy)));
|
|
137
|
+
if (view.report.sourceLineage.manifests.some(({ digest }) => !approved.some(({ sourceManifestDigest }) => sourceManifestDigest === digest)))
|
|
138
|
+
throw new Error("Every selected source manifest requires a policy-validated approved export.");
|
|
139
|
+
// Semantic assertions, human prose, evidence records and operator narrative have
|
|
140
|
+
// no portable sharing classification yet. The shareable surface excludes them.
|
|
141
|
+
const { operatorNarrative: _narrative, grafanaUrl: _grafana, atlasUrl: _atlas, ...metadata } = view;
|
|
142
|
+
const correlations = source.input.corpusEntries.flatMap(({ runId, attemptId, bundleId }) => [runId, attemptId, bundleId].filter((value) => value !== undefined));
|
|
143
|
+
const sanitized = sanitizeDerivedExport({ ...metadata, title: "Approved cohort summary", mode: sharing.policy.sharingClass, cases: [], matchingCases: null, reviewPackets: [], filterOptions: {}, filters: view.filters, report: { ...view.report, groups: view.report.groups.map(({ dimensions, metrics, variations }) => ({ dimensions, metrics: metrics.filter(({ population }) => population !== "assertion" && population !== "reviewed-assertion"), variations })), comparisons: [], sourceLineage: { ...view.report.sourceLineage, assertions: [], calibrations: [], comparisonGates: [] }, limitations: [...view.report.limitations, "Sharing excludes semantic findings, human decisions, native content, local links and operator narrative: those fields lack export approval."] } }, sharing.policy, correlations);
|
|
144
|
+
return sanitized;
|
|
145
|
+
}
|
|
146
|
+
export async function writeAtlas(requestPath, outputRoot, filters = {}, share = false) {
|
|
147
|
+
const source = await loadAtlas(requestPath);
|
|
148
|
+
const view = await queryAtlas(source, filters);
|
|
149
|
+
const result = share ? await shareAtlas(source, view) : view;
|
|
150
|
+
assertCalibrationDestination([source.corpusRoot, ...source.input.assertions.map(({ bundleRoot }) => bundleRoot)], outputRoot);
|
|
151
|
+
if (statSync(outputRoot, { throwIfNoEntry: false }))
|
|
152
|
+
throw new Error("Atlas output destination already exists.");
|
|
153
|
+
await mkdir(dirname(resolve(outputRoot)), { recursive: true, mode: 0o700 });
|
|
154
|
+
const staging = await mkdtemp(join(dirname(resolve(outputRoot)), ".ebo-atlas-"));
|
|
155
|
+
try {
|
|
156
|
+
await writeFile(join(staging, "index.html"), renderAtlas(result, false), { mode: 0o600, flag: "wx" });
|
|
157
|
+
await writeFile(join(staging, "report.json"), `${canonicalizeMetadata(result)}\n`, { mode: 0o600, flag: "wx" });
|
|
158
|
+
if (!share)
|
|
159
|
+
await writeAtlasGrafana(staging, result);
|
|
160
|
+
await rename(staging, outputRoot);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await rm(staging, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
export async function serveAtlas(requestPath, port = 13011) {
|
|
168
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65535)
|
|
169
|
+
throw new Error("Invalid Atlas port.");
|
|
170
|
+
await loadAtlas(requestPath);
|
|
171
|
+
const server = createServer(async (req, res) => {
|
|
172
|
+
res.setHeader("Cache-Control", "no-store");
|
|
173
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
174
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
175
|
+
try {
|
|
176
|
+
if (req.method !== "GET") {
|
|
177
|
+
res.writeHead(405).end();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!req.headers.host || !["127.0.0.1", "localhost", "[::1]"].includes(new URL(`http://${req.headers.host}`).hostname)) {
|
|
181
|
+
res.writeHead(403).end();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (req.headers.origin && !["127.0.0.1", "localhost", "[::1]"].includes(new URL(req.headers.origin).hostname)) {
|
|
185
|
+
res.writeHead(403).end();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
189
|
+
if (!["/", "/api/view", "/api/metrics", "/api/behaviors", "/api/behavior-chart", "/api/comparisons", "/api/cases", "/report.html", "/report.json"].includes(url.pathname)) {
|
|
190
|
+
res.writeHead(404).end();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const filters = {};
|
|
194
|
+
for (const [key, value] of url.searchParams) {
|
|
195
|
+
if (key === "share")
|
|
196
|
+
continue;
|
|
197
|
+
if (!ATLAS_FILTERS.includes(key))
|
|
198
|
+
throw new Error("Unknown Atlas query parameter.");
|
|
199
|
+
if (value && (key === "q" || value !== "$__all"))
|
|
200
|
+
filters[key] = value;
|
|
201
|
+
}
|
|
202
|
+
const source = await loadAtlas(requestPath);
|
|
203
|
+
let view = await queryAtlas(source, filters);
|
|
204
|
+
if (url.searchParams.get("share") === "true")
|
|
205
|
+
view = await shareAtlas(source, view);
|
|
206
|
+
if (url.pathname === "/" || url.pathname === "/report.html") {
|
|
207
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
208
|
+
res.end(renderAtlas(view, url.pathname === "/"));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
212
|
+
if (url.pathname === "/api/comparisons") {
|
|
213
|
+
res.end(JSON.stringify(view.report.comparisons.map(({ id, measure, matchedDifference, claimStatus, limitations }) => ({ comparison: id, measure, status: claimStatus, numerator: matchedDifference.status === "available" ? matchedDifference.numerator.value : null, denominator: matchedDifference.denominator.value, unit: matchedDifference.denominator.unit, exclusions: matchedDifference.exclusions.map(({ reason, count }) => `${reason}: ${count}`).join("; "), limitations: limitations.join(" ") }))));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const rows = url.pathname === "/api/metrics" ? view.report.groups.flatMap((group) => group.metrics.map((metric) => ({ group: Object.values(group.dimensions).join(" · "), metric: metric.id, status: metric.measurement.status, numerator: metric.measurement.numerator.value, denominator: metric.measurement.denominator.value, unit: metric.measurement.denominator.unit, exclusions: metric.measurement.exclusions.map(({ reason, count }) => `${reason}: ${count}`).join("; "), cohortDigest: view.cohortDigest }))) : view;
|
|
217
|
+
res.end(JSON.stringify(url.pathname === "/api/behavior-chart" ? atlasBehaviorRows(view).map(({ label, numerator }) => ({ label, count: numerator })) : url.pathname === "/api/behaviors" ? atlasBehaviorRows(view) : url.pathname === "/api/cases" ? view.cases.map(({ runId, attemptId, model, harness, task, trial, category, assessment, review, key }) => ({ runId, attemptId, model, harness, task, trial, category, assessment, review, href: `http://${req.headers.host}/?${new URLSearchParams(filters)}#case-${key}` })) : rows));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
222
|
+
res.end(error instanceof Error ? error.message : "Atlas query failed.");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
await new Promise((accept, reject) => { server.once("error", reject); server.listen(port, "127.0.0.1", accept); });
|
|
226
|
+
return server;
|
|
227
|
+
}
|
|
228
|
+
function readJson(path) { const text = readBoundedFile(path).toString("utf8"); assertNoDuplicateJsonKeys(text); return JSON.parse(text); }
|
|
229
|
+
function displaySafe(value) { return sanitizeDerivedExport(value, { sharingClass: "partner", maxArtifactBytes: 16 * 1024 * 1024, maxStringBytes: 128 * 1024 }); }
|
|
230
|
+
function readArtifact(path) { const value = readJson(path); const errors = validateArtifact(path, value); if (errors.length)
|
|
231
|
+
throw new Error(`Invalid Atlas source: ${errors[0].field} ${errors[0].message}`); return value; }
|
|
232
|
+
function exactKeys(value, keys) { if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !keys.includes(key)))
|
|
233
|
+
throw new Error("Unknown or invalid Atlas fields."); }
|
|
234
|
+
function localUrl(value) { const url = new URL(value); if (url.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) || url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
235
|
+
throw new Error("Atlas integration URL must be a local HTTP origin."); return url.origin; }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type AgentSdkNativeRecord } from "./agent-sdk-normalizer.js";
|
|
2
|
+
import { type AdapterCoverageReport, type NormalizedDataset } from "./normalization-integrity.js";
|
|
3
|
+
import type { NativeEvidenceReference, NativeEvidenceResolution, NativeEvidenceResolver, NormalizationInput } from "./uniform-events.js";
|
|
4
|
+
type DigestString = `sha256:${string}`;
|
|
5
|
+
type VersionedIdentity = {
|
|
6
|
+
id: string;
|
|
7
|
+
version: string;
|
|
8
|
+
};
|
|
9
|
+
export type BehaviorVocabulary = {
|
|
10
|
+
schemaVersion: "ebo.behavior-vocabulary/v1";
|
|
11
|
+
version: string;
|
|
12
|
+
categories: ReadonlyArray<{
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
dimensions: ReadonlyArray<{
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}>;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
export type BehaviorCitation = {
|
|
22
|
+
eventId: string;
|
|
23
|
+
nativeReference: NativeEvidenceReference;
|
|
24
|
+
};
|
|
25
|
+
type AssessedJudgment = {
|
|
26
|
+
disposition: "assessed";
|
|
27
|
+
assessment: "constructive" | "adverse" | "mixed" | "context-dependent";
|
|
28
|
+
confidence: {
|
|
29
|
+
value: number;
|
|
30
|
+
scale: "evaluator-reported-0-to-1";
|
|
31
|
+
};
|
|
32
|
+
rationale: string;
|
|
33
|
+
alternativeExplanation: string;
|
|
34
|
+
citations: readonly BehaviorCitation[];
|
|
35
|
+
};
|
|
36
|
+
type AbstainedJudgment = {
|
|
37
|
+
disposition: "abstained";
|
|
38
|
+
reason: string;
|
|
39
|
+
missingEvidenceCapability?: string;
|
|
40
|
+
rationale: string;
|
|
41
|
+
alternativeExplanation: string;
|
|
42
|
+
citations: readonly BehaviorCitation[];
|
|
43
|
+
};
|
|
44
|
+
export type BehaviorAssertion = {
|
|
45
|
+
schemaVersion: "ebo.behavior-assertion/v1";
|
|
46
|
+
id: string;
|
|
47
|
+
runId: string;
|
|
48
|
+
attemptId: string;
|
|
49
|
+
dataset: {
|
|
50
|
+
schemaVersion: "ebo.normalized-dataset/v1";
|
|
51
|
+
digest: DigestString;
|
|
52
|
+
};
|
|
53
|
+
behavior: {
|
|
54
|
+
vocabularyVersion: string;
|
|
55
|
+
categoryId: string;
|
|
56
|
+
dimensionId: string;
|
|
57
|
+
};
|
|
58
|
+
rubric: VersionedIdentity;
|
|
59
|
+
evaluator: VersionedIdentity & {
|
|
60
|
+
configurationDigest?: DigestString;
|
|
61
|
+
};
|
|
62
|
+
judgment: AssessedJudgment | AbstainedJudgment;
|
|
63
|
+
};
|
|
64
|
+
export type BehaviorReview = {
|
|
65
|
+
schemaVersion: "ebo.behavior-review/v1";
|
|
66
|
+
id: string;
|
|
67
|
+
assertion: {
|
|
68
|
+
id: string;
|
|
69
|
+
schemaVersion: "ebo.behavior-assertion/v1";
|
|
70
|
+
digest: DigestString;
|
|
71
|
+
};
|
|
72
|
+
state: "proposed" | "confirmed" | "disputed" | "rejected" | "insufficient-evidence";
|
|
73
|
+
reviewer?: {
|
|
74
|
+
kind: "human";
|
|
75
|
+
id: string;
|
|
76
|
+
};
|
|
77
|
+
rationale?: string;
|
|
78
|
+
};
|
|
79
|
+
export type ResolvedBehaviorCitation = BehaviorCitation & {
|
|
80
|
+
resolution: NativeEvidenceResolution;
|
|
81
|
+
};
|
|
82
|
+
export type AgentSdkBehaviorEvidence = {
|
|
83
|
+
capture: NormalizationInput<AgentSdkNativeRecord>;
|
|
84
|
+
dataset: NormalizedDataset;
|
|
85
|
+
resolver: NativeEvidenceResolver;
|
|
86
|
+
coverage: AdapterCoverageReport;
|
|
87
|
+
};
|
|
88
|
+
export declare const DEFAULT_BEHAVIOR_VOCABULARY: BehaviorVocabulary;
|
|
89
|
+
export declare function validateBehaviorAssertion(assertion: BehaviorAssertion, dataset: NormalizedDataset, resolver: NativeEvidenceResolver, vocabulary?: BehaviorVocabulary): Promise<readonly ResolvedBehaviorCitation[]>;
|
|
90
|
+
export declare function validateBehaviorReview(assertion: BehaviorAssertion, review: BehaviorReview): void;
|
|
91
|
+
export declare function isConfirmedBehaviorAssertion(assertion: BehaviorAssertion, dataset: NormalizedDataset, resolver: NativeEvidenceResolver, review?: BehaviorReview, vocabulary?: BehaviorVocabulary): Promise<boolean>;
|
|
92
|
+
export declare function validateAgentSdkBehaviorAssertion(bundleRoot: string, assertion: BehaviorAssertion, review?: BehaviorReview): Promise<readonly ResolvedBehaviorCitation[]>;
|
|
93
|
+
export declare function createAgentSdkBehaviorEvidence(bundleRoot: string): Promise<AgentSdkBehaviorEvidence>;
|
|
94
|
+
export declare function validateRetainedBehaviorAssertion(bundleRoot: string, assertion: BehaviorAssertion, review?: BehaviorReview): Promise<readonly ResolvedBehaviorCitation[]>;
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRetainedBehaviorEvidence } from "./retained-evidence.js";
|
|
3
|
+
import { CLAUDE_AGENT_SDK_NORMALIZATION_ADAPTER_VERSION, claudeAgentSdkNormalizationAdapter, createAgentSdkNativeEvidenceResolver, readQualifiedClaudeAgentSdkCapture, } from "./agent-sdk-normalizer.js";
|
|
4
|
+
import { canonicalizeMetadata, digestMetadata, validateArtifact } from "./artifacts.js";
|
|
5
|
+
import { describeNormalizedDataset, validateNormalizedDataset, } from "./normalization-integrity.js";
|
|
6
|
+
export const DEFAULT_BEHAVIOR_VOCABULARY = loadDefaultVocabulary();
|
|
7
|
+
export async function validateBehaviorAssertion(assertion, dataset, resolver, vocabulary = DEFAULT_BEHAVIOR_VOCABULARY) {
|
|
8
|
+
assertValid("behavior assertion", assertion);
|
|
9
|
+
assertVocabulary(vocabulary);
|
|
10
|
+
await validateNormalizedDataset(dataset, resolver);
|
|
11
|
+
const datasetDigest = digest(dataset);
|
|
12
|
+
if (assertion.dataset.schemaVersion !== dataset.schemaVersion || assertion.dataset.digest !== datasetDigest) {
|
|
13
|
+
throw new Error("Behavior assertion normalized dataset version or digest does not match.");
|
|
14
|
+
}
|
|
15
|
+
if (assertion.runId !== dataset.runId || assertion.attemptId !== dataset.attemptId) {
|
|
16
|
+
throw new Error("Behavior assertion belongs to the wrong run or attempt.");
|
|
17
|
+
}
|
|
18
|
+
if (assertion.behavior.vocabularyVersion !== vocabulary.version) {
|
|
19
|
+
throw new Error("Behavior assertion uses the wrong vocabulary version.");
|
|
20
|
+
}
|
|
21
|
+
const category = vocabulary.categories.find(({ id }) => id === assertion.behavior.categoryId);
|
|
22
|
+
if (category === undefined || !category.dimensions.some(({ id }) => id === assertion.behavior.dimensionId)) {
|
|
23
|
+
throw new Error("Behavior assertion does not identify one declared behavior dimension.");
|
|
24
|
+
}
|
|
25
|
+
const eventIds = new Set();
|
|
26
|
+
const events = new Map(dataset.events.map((event) => [event.id, event]));
|
|
27
|
+
const records = new Map(dataset.nativeRecords.map((record) => [referenceKey(record.reference), record]));
|
|
28
|
+
const resolved = [];
|
|
29
|
+
for (const citation of assertion.judgment.citations) {
|
|
30
|
+
if (eventIds.has(citation.eventId))
|
|
31
|
+
throw new Error(`Behavior assertion cites event "${citation.eventId}" more than once.`);
|
|
32
|
+
eventIds.add(citation.eventId);
|
|
33
|
+
const event = events.get(citation.eventId);
|
|
34
|
+
if (event === undefined)
|
|
35
|
+
throw new Error(`Behavior assertion cites unknown normalized event "${citation.eventId}".`);
|
|
36
|
+
if (referenceKey(citation.nativeReference) !== referenceKey(event.source.nativeReference)) {
|
|
37
|
+
throw new Error(`Behavior assertion citation for event "${citation.eventId}" does not match its native source.`);
|
|
38
|
+
}
|
|
39
|
+
const nativeRecord = records.get(referenceKey(citation.nativeReference));
|
|
40
|
+
if (nativeRecord === undefined) {
|
|
41
|
+
throw new Error(`Behavior assertion citation for event "${citation.eventId}" has no captured native record.`);
|
|
42
|
+
}
|
|
43
|
+
const resolution = await resolver.resolve(citation.nativeReference);
|
|
44
|
+
if (typeof resolution !== "object" || resolution.runId !== dataset.runId
|
|
45
|
+
|| resolution.attemptId !== dataset.attemptId || resolution.digest !== nativeRecord.digest) {
|
|
46
|
+
throw new Error(`Behavior assertion citation for event "${citation.eventId}" is stale or belongs to another attempt.`);
|
|
47
|
+
}
|
|
48
|
+
resolved.push({ ...structuredClone(citation), resolution });
|
|
49
|
+
}
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
export function validateBehaviorReview(assertion, review) {
|
|
53
|
+
assertValid("behavior assertion", assertion);
|
|
54
|
+
assertValid("behavior review", review);
|
|
55
|
+
if (review.assertion.id !== assertion.id || review.assertion.schemaVersion !== assertion.schemaVersion
|
|
56
|
+
|| review.assertion.digest !== digest(assertion)) {
|
|
57
|
+
throw new Error("Behavior review assertion identity or digest does not match.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function isConfirmedBehaviorAssertion(assertion, dataset, resolver, review, vocabulary = DEFAULT_BEHAVIOR_VOCABULARY) {
|
|
61
|
+
await validateBehaviorAssertion(assertion, dataset, resolver, vocabulary);
|
|
62
|
+
if (review === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
validateBehaviorReview(assertion, review);
|
|
65
|
+
return assertion.judgment.disposition === "assessed" && review.state === "confirmed";
|
|
66
|
+
}
|
|
67
|
+
export async function validateAgentSdkBehaviorAssertion(bundleRoot, assertion, review) {
|
|
68
|
+
const { dataset, resolver } = await createAgentSdkBehaviorEvidence(bundleRoot);
|
|
69
|
+
const citations = await validateBehaviorAssertion(assertion, dataset, resolver);
|
|
70
|
+
if (review !== undefined)
|
|
71
|
+
validateBehaviorReview(assertion, review);
|
|
72
|
+
return citations;
|
|
73
|
+
}
|
|
74
|
+
export async function createAgentSdkBehaviorEvidence(bundleRoot) {
|
|
75
|
+
const capture = await readQualifiedClaudeAgentSdkCapture(bundleRoot);
|
|
76
|
+
const normalization = await claudeAgentSdkNormalizationAdapter.normalize(capture);
|
|
77
|
+
const dataset = describeNormalizedDataset({
|
|
78
|
+
capture,
|
|
79
|
+
normalization,
|
|
80
|
+
capabilityProfile: normalization.capabilityProfile,
|
|
81
|
+
adapterVersion: CLAUDE_AGENT_SDK_NORMALIZATION_ADAPTER_VERSION,
|
|
82
|
+
nativeType: agentSdkNativeType,
|
|
83
|
+
contentDigest: (reference) => agentSdkContentDigest(capture, reference),
|
|
84
|
+
});
|
|
85
|
+
const resolver = createAgentSdkNativeEvidenceResolver(capture);
|
|
86
|
+
const coverage = await validateNormalizedDataset(dataset, resolver);
|
|
87
|
+
return { capture, dataset, resolver, coverage };
|
|
88
|
+
}
|
|
89
|
+
export async function validateRetainedBehaviorAssertion(bundleRoot, assertion, review) {
|
|
90
|
+
const { dataset, resolver } = await createRetainedBehaviorEvidence(bundleRoot);
|
|
91
|
+
const citations = await validateBehaviorAssertion(assertion, dataset, resolver);
|
|
92
|
+
if (review !== undefined)
|
|
93
|
+
validateBehaviorReview(assertion, review);
|
|
94
|
+
return citations;
|
|
95
|
+
}
|
|
96
|
+
function assertVocabulary(vocabulary) {
|
|
97
|
+
assertValid("behavior vocabulary", vocabulary);
|
|
98
|
+
const categoryIds = vocabulary.categories.map(({ id }) => id);
|
|
99
|
+
if (new Set(categoryIds).size !== categoryIds.length)
|
|
100
|
+
throw new Error("Behavior vocabulary category IDs must be unique.");
|
|
101
|
+
for (const category of vocabulary.categories) {
|
|
102
|
+
const dimensionIds = category.dimensions.map(({ id }) => id);
|
|
103
|
+
if (new Set(dimensionIds).size !== dimensionIds.length) {
|
|
104
|
+
throw new Error(`Behavior vocabulary dimension IDs in category "${category.id}" must be unique.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function loadDefaultVocabulary() {
|
|
109
|
+
const vocabulary = JSON.parse(readFileSync(new URL("../../ontology/behavior-categories.v1.json", import.meta.url), "utf8"));
|
|
110
|
+
assertVocabulary(vocabulary);
|
|
111
|
+
return vocabulary;
|
|
112
|
+
}
|
|
113
|
+
function digest(value) {
|
|
114
|
+
return `sha256:${digestMetadata(value).value}`;
|
|
115
|
+
}
|
|
116
|
+
function referenceKey(reference) {
|
|
117
|
+
return canonicalizeMetadata(reference);
|
|
118
|
+
}
|
|
119
|
+
function assertValid(label, value) {
|
|
120
|
+
const errors = validateArtifact(label, value);
|
|
121
|
+
if (errors.length > 0)
|
|
122
|
+
throw new Error(errors.map(({ field, message }) => `${label} ${field}: ${message}`).join("\n"));
|
|
123
|
+
}
|
|
124
|
+
function agentSdkNativeType(record) {
|
|
125
|
+
const document = asRecord(record.document);
|
|
126
|
+
if (record.kind === "session")
|
|
127
|
+
return text(document?.nativeType) ?? "session";
|
|
128
|
+
if (record.kind === "hook")
|
|
129
|
+
return text(document?.hook) ?? "hook";
|
|
130
|
+
return {
|
|
131
|
+
telemetry: "agent-sdk-telemetry",
|
|
132
|
+
workspace: "workspace-outcome",
|
|
133
|
+
verifier: "verifier-result",
|
|
134
|
+
"assessment-mode": "assessment-mode",
|
|
135
|
+
manifest: "terminal-record",
|
|
136
|
+
}[record.kind] ?? record.kind;
|
|
137
|
+
}
|
|
138
|
+
function agentSdkContentDigest(capture, reference) {
|
|
139
|
+
for (const { record } of capture.records) {
|
|
140
|
+
if (record.kind !== "workspace" && record.kind !== "diagnostic")
|
|
141
|
+
continue;
|
|
142
|
+
const descriptor = asRecord(record.document);
|
|
143
|
+
if (descriptor?.id === reference.artifactId && typeof descriptor.digest === "string"
|
|
144
|
+
&& /^sha256:[a-f0-9]{64}$/u.test(descriptor.digest))
|
|
145
|
+
return descriptor.digest;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function asRecord(value) {
|
|
150
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
151
|
+
}
|
|
152
|
+
function text(value) {
|
|
153
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
154
|
+
}
|