create-agdf 0.11.3 → 0.12.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/README.md +5 -4
- package/generated/.agdf/control/templates/MASTER_BACKLOG.md +1 -1
- package/generated/.agdf/control/templates/artefacts/BROWNFIELD_REVIEW.md +25 -0
- package/generated/.github/skills/agdf-brownfield-analysis/SKILL.md +26 -0
- package/generated/.github/skills/agdf-gate-check/SKILL.md +40 -18
- package/generated/.github/skills/agdf-interaction-locales.json +50 -0
- package/generated/.github/skills/agdf-runtime-contract.md +1 -0
- package/generated/.github/skills/contracts/control-scaffold.md +2 -0
- package/generated/.github/skills/contracts/gate-transition.md +7 -3
- package/generated/.github/skills/contracts/interaction.md +22 -0
- package/generated/.github/skills/contracts/modes.md +81 -0
- package/generated/.github/skills/contracts/task-target-resolution.md +102 -0
- package/generated/.opencode/AGDF.md +2 -0
- package/generated/.opencode/agdf-interaction-locales.json +50 -0
- package/generated/.opencode/agdf-runtime-contract.md +1 -0
- package/generated/.opencode/contracts/control-scaffold.md +2 -0
- package/generated/.opencode/contracts/gate-transition.md +7 -3
- package/generated/.opencode/contracts/interaction.md +22 -0
- package/generated/.opencode/contracts/modes.md +81 -0
- package/generated/.opencode/contracts/task-target-resolution.md +102 -0
- package/generated/.opencode/skills/agdf-brownfield-analysis/SKILL.md +26 -0
- package/generated/.opencode/skills/agdf-gate-check/SKILL.md +40 -18
- package/generated/AGENTS.md +1 -0
- package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/control/templates/MASTER_BACKLOG.md +1 -1
- package/generated/plugins/agdf/control/templates/artefacts/BROWNFIELD_REVIEW.md +25 -0
- package/generated/plugins/agdf/meta/agdf-agent-router.md +16 -0
- package/generated/plugins/agdf/meta/agdf-interaction-locales.json +50 -0
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +2 -1
- package/generated/plugins/agdf/meta/agdf-runtime-contract.md +1 -0
- package/generated/plugins/agdf/meta/contracts/control-scaffold.md +2 -0
- package/generated/plugins/agdf/meta/contracts/gate-transition.md +7 -3
- package/generated/plugins/agdf/meta/contracts/interaction.md +22 -0
- package/generated/plugins/agdf/meta/contracts/modes.md +81 -0
- package/generated/plugins/agdf/meta/contracts/task-target-resolution.md +102 -0
- package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +50 -0
- package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +2 -1
- package/generated/plugins/agdf/runtime/create-agdf/lib/cli/command-registry.js +2 -2
- package/generated/plugins/agdf/runtime/create-agdf/lib/cli/delivery-path-search-command.js +78 -14
- package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-policy.js +14 -7
- package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/shared.js +1 -0
- package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/claude.js +2 -11
- package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/codex.js +2 -11
- package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/opencode.js +234 -0
- package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/prompt.js +13 -0
- package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/search-engine.js +1 -0
- package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/surfaces/capabilities.js +15 -2
- package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +85 -0
- package/generated/plugins/agdf/runtime/create-agdf/package.json +6 -2
- package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
- package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +119 -3
- package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +26 -0
- package/generated/plugins/agdf/skills/gate-check/SKILL.md +40 -18
- package/lib/cli/application.js +29 -3
- package/lib/cli/command-registry.js +2 -2
- package/lib/cli/delivery-path-search-command.js +78 -14
- package/lib/control-evaluation/gate-policy.js +14 -7
- package/lib/control-evaluation/shared.js +1 -0
- package/lib/delivery-path-search/evaluators/claude.js +2 -11
- package/lib/delivery-path-search/evaluators/codex.js +2 -11
- package/lib/delivery-path-search/evaluators/opencode.js +234 -0
- package/lib/delivery-path-search/evaluators/prompt.js +13 -0
- package/lib/delivery-path-search/search-engine.js +1 -0
- package/lib/delivery-path-search/surfaces/capabilities.js +15 -2
- package/lib/installers/opencode.js +341 -10
- package/lib/interaction-presentation.js +85 -0
- package/lib/lifecycle/operations.js +4 -0
- package/lib/live-agent/read-only-structured.js +43 -0
- package/lib/proportionality-benchmark/blind-prompt.js +37 -0
- package/lib/proportionality-benchmark/contracts.js +120 -0
- package/lib/proportionality-benchmark/corpus-loader.js +110 -0
- package/lib/proportionality-benchmark/evaluator.js +250 -0
- package/lib/proportionality-benchmark/index.js +7 -0
- package/lib/proportionality-benchmark/live-recorder.js +100 -0
- package/lib/proportionality-benchmark/report.js +51 -0
- package/lib/proportionality-benchmark/source-fingerprint.js +30 -0
- package/lib/skill-evals/live-recorder.js +3 -26
- package/opencode-plugin.js +59 -3
- package/package.json +6 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DELIVERY_PATHS } from "./contracts.js";
|
|
2
|
+
|
|
3
|
+
export function buildBlindPrompt(testCase, behaviorSources) {
|
|
4
|
+
if (testCase.profile_id === "staged-v2") {
|
|
5
|
+
return [
|
|
6
|
+
"Perform a read-only, non-authorizing AGDF benchmark classification. Do not modify files, invoke tools, persist control state, or claim a real approval.",
|
|
7
|
+
"Synthetic fixture state is measurement input only. Return only JSON matching the supplied schema.",
|
|
8
|
+
`Allowed stages: ${testCase.allowed_stages.join(", ")}.`,
|
|
9
|
+
`Allowed paths: ${DELIVERY_PATHS.join(", ")}.`,
|
|
10
|
+
`Requested axes: ${testCase.requested_axes.join(", ")}. Every requested axis must use evaluated with one non-null allowed value. Non-requested axes must be null with not_evaluable_yet.`,
|
|
11
|
+
"eventual_delivery_path is a benchmark candidate, never an operative Mode/Slice Decision.",
|
|
12
|
+
"compact_delivery requires visible approved UR, completed Brownfield analysis, and stored quick_task context in decision_grounds.",
|
|
13
|
+
`Scenario ID: ${testCase.scenario_id}`,
|
|
14
|
+
`Case ID: ${testCase.case_id}`,
|
|
15
|
+
`Lifecycle stage: ${testCase.lifecycle_stage}`,
|
|
16
|
+
`Task: ${testCase.task_summary}`,
|
|
17
|
+
`Repository context: ${testCase.repository_context}`,
|
|
18
|
+
`Control-state context: ${testCase.control_state_context}`,
|
|
19
|
+
`Evidence pack: ${JSON.stringify(testCase.evidence_pack ?? {})}`,
|
|
20
|
+
"Current canonical routing sources follow:",
|
|
21
|
+
behaviorSources,
|
|
22
|
+
].join("\n\n");
|
|
23
|
+
}
|
|
24
|
+
return [
|
|
25
|
+
"Classify the delivery path AGDF would select for this task. Do not modify files or invoke tools.",
|
|
26
|
+
"Return only JSON matching the supplied schema. Do not reveal hidden reasoning.",
|
|
27
|
+
`Allowed paths: ${DELIVERY_PATHS.join(", ")}.`,
|
|
28
|
+
"Use null plus ambiguous=true when the evidence does not support one path.",
|
|
29
|
+
"compact_delivery requires decision_grounds that visibly reference approved UR, completed Brownfield analysis, and stored quick_task context.",
|
|
30
|
+
`Case ID: ${testCase.case_id}`,
|
|
31
|
+
`Task: ${testCase.task_summary}`,
|
|
32
|
+
`Repository context: ${testCase.repository_context}`,
|
|
33
|
+
`Control-state context: ${testCase.control_state_context}`,
|
|
34
|
+
"Current canonical routing sources follow:",
|
|
35
|
+
behaviorSources,
|
|
36
|
+
].join("\n\n");
|
|
37
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const DELIVERY_PATHS = Object.freeze([
|
|
2
|
+
"trivial_change", "quick_task", "compact_delivery",
|
|
3
|
+
"verified_change", "structured_slice", "structured_delivery",
|
|
4
|
+
]);
|
|
5
|
+
export const PATH_RANK = Object.freeze(Object.fromEntries(DELIVERY_PATHS.map((path, index) => [path, index])));
|
|
6
|
+
export const STAGES = Object.freeze([
|
|
7
|
+
"ungated_execution", "ur", "brownfield_review", "prd", "sd", "tp",
|
|
8
|
+
"brownfield_analysis", "cd_tests", "cr", "qa", "uat", "or", "blocked",
|
|
9
|
+
]);
|
|
10
|
+
export const OBSERVATION_SCHEMA = Object.freeze({
|
|
11
|
+
type: "object",
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
required: ["schema_version", "observed_delivery_path", "ambiguous", "rationale", "decision_grounds"],
|
|
14
|
+
properties: {
|
|
15
|
+
schema_version: { type: "string", const: "1" },
|
|
16
|
+
observed_delivery_path: { type: ["string", "null"], enum: [...DELIVERY_PATHS, null] },
|
|
17
|
+
ambiguous: { type: "boolean" },
|
|
18
|
+
rationale: { type: "string", maxLength: 600 },
|
|
19
|
+
decision_grounds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 120 } },
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
export const STAGED_OBSERVATION_SCHEMA = Object.freeze({
|
|
23
|
+
type: "object",
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
required: [
|
|
26
|
+
"schema_version", "observed_next_permissible_stage", "stage_evaluability",
|
|
27
|
+
"observed_delivery_path", "path_evaluability", "rationale", "decision_grounds",
|
|
28
|
+
],
|
|
29
|
+
properties: {
|
|
30
|
+
schema_version: { type: "string", const: "2" },
|
|
31
|
+
observed_next_permissible_stage: { type: ["string", "null"], enum: [...STAGES, null] },
|
|
32
|
+
stage_evaluability: { type: "string", enum: ["evaluated", "not_evaluable_yet"] },
|
|
33
|
+
observed_delivery_path: { type: ["string", "null"], enum: [...DELIVERY_PATHS, null] },
|
|
34
|
+
path_evaluability: { type: "string", enum: ["evaluated", "not_evaluable_yet"] },
|
|
35
|
+
rationale: { type: "string", maxLength: 600 },
|
|
36
|
+
decision_grounds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 120 } },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const forbiddenBaselineKeys = new Set(["observed_delivery_path", "classification", "status", "passed"]);
|
|
41
|
+
const forbiddenText = /(api[_-]?key|access[_-]?token|cookie|authorization:\s*bearer|hidden reasoning|\/Users\/[^/\s]+|[A-Z]:\\Users\\[^\\\s]+)/i;
|
|
42
|
+
export function fail(message, code = "PROPORTIONALITY_SCHEMA_INVALID") {
|
|
43
|
+
throw Object.assign(new Error(message), { code });
|
|
44
|
+
}
|
|
45
|
+
export function stable(value) {
|
|
46
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
47
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export function validateBaseline(value) {
|
|
51
|
+
if (value?.schema_version !== "1" || value?.baseline_version !== "1.0.0") fail("unsupported baseline");
|
|
52
|
+
if (!Array.isArray(value.cases) || value.cases.length !== 40) fail("baseline must contain exactly 40 cases");
|
|
53
|
+
const ids = new Set();
|
|
54
|
+
for (const item of value.cases) {
|
|
55
|
+
if (!/^PB-\d{3}$/.test(item.case_id) || ids.has(item.case_id)) fail(`invalid or duplicate case ${item.case_id}`);
|
|
56
|
+
if (!DELIVERY_PATHS.includes(item.expected_delivery_path)) fail(`unknown path for ${item.case_id}`);
|
|
57
|
+
if (!item.task_summary || !item.rationale || !item.evidence_ref || item.evidence_ref.startsWith("/")) fail(`incomplete case ${item.case_id}`);
|
|
58
|
+
for (const key of forbiddenBaselineKeys) if (key in item) fail(`baseline contains forbidden field ${key}`);
|
|
59
|
+
ids.add(item.case_id);
|
|
60
|
+
}
|
|
61
|
+
if (new Set(value.cases.map((item) => item.expected_delivery_path)).size !== DELIVERY_PATHS.length) fail("baseline path coverage incomplete");
|
|
62
|
+
if (value.cases.filter((item) => item.adversarial).length < 10) fail("adversarial coverage too small");
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
export function normalizeAgentOutput(raw) {
|
|
66
|
+
let value;
|
|
67
|
+
try { value = typeof raw === "string" ? JSON.parse(raw) : raw; }
|
|
68
|
+
catch { fail("agent output is not JSON", "PROPORTIONALITY_OUTPUT_INVALID"); }
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("agent output is not an object", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
70
|
+
const allowedKeys = new Set(["schema_version", "observed_delivery_path", "ambiguous", "rationale", "decision_grounds"]);
|
|
71
|
+
if (Object.keys(value).some((key) => !allowedKeys.has(key))) fail("agent output contains unknown fields", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
72
|
+
if (value.schema_version !== "1" || typeof value.ambiguous !== "boolean") fail("agent output contract mismatch", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
73
|
+
if (value.observed_delivery_path !== null && !DELIVERY_PATHS.includes(value.observed_delivery_path)) fail("unknown observed path", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
74
|
+
if (typeof value.rationale !== "string" || value.rationale.length > 600 || forbiddenText.test(value.rationale)) fail("unsafe rationale", "PROPORTIONALITY_REDACTION_FAILED");
|
|
75
|
+
if (!Array.isArray(value.decision_grounds) || value.decision_grounds.length > 12 || value.decision_grounds.some((item) => typeof item !== "string" || item.length > 120 || forbiddenText.test(item))) fail("unsafe decision grounds", "PROPORTIONALITY_REDACTION_FAILED");
|
|
76
|
+
let path = value.observed_delivery_path;
|
|
77
|
+
let ambiguous = value.ambiguous || path === null;
|
|
78
|
+
if (path === "compact_delivery") {
|
|
79
|
+
const grounds = value.decision_grounds.map((item) => item.toLowerCase()).join(" ");
|
|
80
|
+
if (!grounds.includes("ur") || !grounds.includes("brownfield") || !grounds.includes("quick_task")) {
|
|
81
|
+
path = null;
|
|
82
|
+
ambiguous = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (ambiguous) path = null;
|
|
86
|
+
return { schema_version: "1", observed_delivery_path: path, ambiguous, rationale: value.rationale, decision_grounds: [...value.decision_grounds] };
|
|
87
|
+
}
|
|
88
|
+
export function normalizeStagedAgentOutput(raw, requestedAxes) {
|
|
89
|
+
let value;
|
|
90
|
+
try { value = typeof raw === "string" ? JSON.parse(raw) : raw; }
|
|
91
|
+
catch { fail("agent output is not JSON", "PROPORTIONALITY_OUTPUT_INVALID"); }
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("agent output is not an object", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
93
|
+
const allowed = new Set(Object.keys(STAGED_OBSERVATION_SCHEMA.properties));
|
|
94
|
+
if (Object.keys(value).some((key) => !allowed.has(key))) fail("agent output contains unknown fields", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
95
|
+
if (value.schema_version !== "2" || (!STAGES.includes(value.observed_next_permissible_stage) && value.observed_next_permissible_stage !== null)) fail("invalid staged contract", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
96
|
+
if (!DELIVERY_PATHS.includes(value.observed_delivery_path) && value.observed_delivery_path !== null) fail("invalid staged path", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
97
|
+
if (!["evaluated", "not_evaluable_yet"].includes(value.stage_evaluability) || !["evaluated", "not_evaluable_yet"].includes(value.path_evaluability)) fail("invalid staged evaluability", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
98
|
+
for (const [axis, observed, evaluability] of [
|
|
99
|
+
["next_permissible_stage", value.observed_next_permissible_stage, value.stage_evaluability],
|
|
100
|
+
["eventual_delivery_path", value.observed_delivery_path, value.path_evaluability],
|
|
101
|
+
]) {
|
|
102
|
+
const requested = requestedAxes.includes(axis);
|
|
103
|
+
if (requested !== (evaluability === "evaluated") || (requested ? observed === null : observed !== null)) fail(`axis invariant failed: ${axis}`, "PROPORTIONALITY_OUTPUT_INVALID");
|
|
104
|
+
}
|
|
105
|
+
if (typeof value.rationale !== "string" || value.rationale.length > 600 || forbiddenText.test(value.rationale)) fail("unsafe rationale", "PROPORTIONALITY_REDACTION_FAILED");
|
|
106
|
+
if (!Array.isArray(value.decision_grounds) || value.decision_grounds.length > 12 || value.decision_grounds.some((item) => typeof item !== "string" || item.length > 120 || forbiddenText.test(item))) fail("unsafe decision grounds", "PROPORTIONALITY_REDACTION_FAILED");
|
|
107
|
+
if (value.observed_delivery_path === "compact_delivery") {
|
|
108
|
+
const grounds = value.decision_grounds.join(" ").toLowerCase();
|
|
109
|
+
if (!grounds.includes("ur") || !grounds.includes("brownfield") || !grounds.includes("quick_task")) fail("compact delivery evidence missing", "PROPORTIONALITY_OUTPUT_INVALID");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
schema_version: "2",
|
|
113
|
+
observed_next_permissible_stage: value.observed_next_permissible_stage,
|
|
114
|
+
stage_evaluability: value.stage_evaluability,
|
|
115
|
+
observed_delivery_path: value.observed_delivery_path,
|
|
116
|
+
path_evaluability: value.path_evaluability,
|
|
117
|
+
rationale: value.rationale,
|
|
118
|
+
decision_grounds: [...value.decision_grounds],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fail, validateBaseline } from "./contracts.js";
|
|
5
|
+
|
|
6
|
+
function inside(root, candidate) {
|
|
7
|
+
const target = resolve(root, candidate);
|
|
8
|
+
const rel = relative(root, target);
|
|
9
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep}`)) fail(`unsafe corpus path ${candidate}`);
|
|
10
|
+
return target;
|
|
11
|
+
}
|
|
12
|
+
function existingInside(root, candidate) {
|
|
13
|
+
const target = inside(root, candidate);
|
|
14
|
+
if (!existsSync(target)) fail(`missing corpus path ${candidate}`);
|
|
15
|
+
const canonicalRoot = realpathSync(root);
|
|
16
|
+
const canonicalTarget = realpathSync(target);
|
|
17
|
+
const rel = relative(canonicalRoot, canonicalTarget);
|
|
18
|
+
if (rel === ".." || rel.startsWith(`..${sep}`)) fail(`corpus path escapes through symlink ${candidate}`);
|
|
19
|
+
return target;
|
|
20
|
+
}
|
|
21
|
+
function sha256(path) {
|
|
22
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
const stagedForbidden = /expected_|evidence_ref|classification|threshold|trivial_change|compact_delivery|verified_change|structured_slice|structured_delivery/i;
|
|
25
|
+
export function validateStagedBlindScenario(scenario, evidencePack = null, baselineScenario = null) {
|
|
26
|
+
if (stagedForbidden.test(JSON.stringify(scenario))) fail(`staged scenario leakage ${scenario.scenario_id}`, "PROPORTIONALITY_LEAKAGE");
|
|
27
|
+
const quickTaskReferences = JSON.stringify(scenario).match(/quick_task/gi)?.length ?? 0;
|
|
28
|
+
if (quickTaskReferences && (
|
|
29
|
+
scenario.lifecycle_stage !== "post_brownfield_decision"
|
|
30
|
+
|| scenario.control_state_context !== "A durable UR is approved. Brownfield analysis is complete and the stored mode/slice decision is quick_task."
|
|
31
|
+
|| quickTaskReferences !== 1
|
|
32
|
+
)) fail(`staged scenario path leakage ${scenario.scenario_id}`, "PROPORTIONALITY_LEAKAGE");
|
|
33
|
+
if (evidencePack && stagedForbidden.test(JSON.stringify(evidencePack))) fail(`staged evidence-pack leakage ${scenario.scenario_id}`, "PROPORTIONALITY_LEAKAGE");
|
|
34
|
+
if (baselineScenario && (
|
|
35
|
+
JSON.stringify(scenario).includes(baselineScenario.rationale)
|
|
36
|
+
|| JSON.stringify(evidencePack ?? {}).includes(baselineScenario.rationale)
|
|
37
|
+
)) fail(`staged rationale leakage ${scenario.scenario_id}`, "PROPORTIONALITY_LEAKAGE");
|
|
38
|
+
return scenario;
|
|
39
|
+
}
|
|
40
|
+
export function loadCorpus(repoRoot, profileId = "legacy-v1") {
|
|
41
|
+
const root = existingInside(repoRoot, "evals/proportionality");
|
|
42
|
+
if (profileId === "staged-v2") {
|
|
43
|
+
const manifest = JSON.parse(readFileSync(existingInside(root, "staged-manifest.json"), "utf8"));
|
|
44
|
+
const index = JSON.parse(readFileSync(existingInside(root, manifest.scenarios_path), "utf8"));
|
|
45
|
+
const fixtures = JSON.parse(readFileSync(existingInside(root, manifest.fixture_path), "utf8"));
|
|
46
|
+
const baselinePath = existingInside(repoRoot, manifest.baseline_path);
|
|
47
|
+
const baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
|
|
48
|
+
if (
|
|
49
|
+
manifest.profile_id !== "staged-v2"
|
|
50
|
+
|| baseline.schema_version !== "2"
|
|
51
|
+
|| baseline.baseline_version !== manifest.baseline_version
|
|
52
|
+
|| !manifest.corpus_version
|
|
53
|
+
|| !manifest.fixture_version
|
|
54
|
+
|| fixtures.fixture_version !== manifest.fixture_version
|
|
55
|
+
) fail("staged profile version mismatch");
|
|
56
|
+
if (baseline.cases.length !== 40 || index.scenarios.length !== 72 || new Set(index.scenario_ids).size !== 72) fail("staged corpus coverage mismatch");
|
|
57
|
+
const expectedIds = new Set(baseline.scenarios.map((item) => item.scenario_id));
|
|
58
|
+
if (index.scenarios.some((item) => !expectedIds.has(item.scenario_id)) || baseline.scenarios.some((item) => !index.scenario_ids.includes(item.scenario_id))) fail("staged scenario baseline mismatch");
|
|
59
|
+
for (const scenario of index.scenarios) {
|
|
60
|
+
if (scenario.evidence_pack_id && !fixtures.evidence_packs[scenario.evidence_pack_id]) fail(`missing evidence pack ${scenario.scenario_id}`);
|
|
61
|
+
validateStagedBlindScenario(
|
|
62
|
+
scenario,
|
|
63
|
+
scenario.evidence_pack_id ? fixtures.evidence_packs[scenario.evidence_pack_id] : null,
|
|
64
|
+
baseline.scenarios.find((item) => item.scenario_id === scenario.scenario_id),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const cases = index.scenarios.map((scenario) => ({
|
|
68
|
+
...scenario,
|
|
69
|
+
profile_id: "staged-v2",
|
|
70
|
+
corpus_version: manifest.corpus_version,
|
|
71
|
+
fixture_version: manifest.fixture_version,
|
|
72
|
+
allowed_stages: ["ungated_execution", "ur", "brownfield_review", "prd", "sd", "tp", "brownfield_analysis", "cd_tests", "cr", "qa", "uat", "or", "blocked"],
|
|
73
|
+
evidence_pack: scenario.evidence_pack_id ? fixtures.evidence_packs[scenario.evidence_pack_id] : null,
|
|
74
|
+
}));
|
|
75
|
+
return { profile_id: "staged-v2", manifest, baseline, fixtures, cases };
|
|
76
|
+
}
|
|
77
|
+
if (profileId !== "legacy-v1") fail(`unknown proportionality profile ${profileId}`);
|
|
78
|
+
const manifest = JSON.parse(readFileSync(existingInside(root, "manifest.json"), "utf8"));
|
|
79
|
+
const provenance = JSON.parse(readFileSync(existingInside(root, "legacy-v1-provenance.json"), "utf8"));
|
|
80
|
+
const index = JSON.parse(readFileSync(existingInside(root, "cases.json"), "utf8"));
|
|
81
|
+
const fixtures = JSON.parse(readFileSync(existingInside(root, "fixtures/catalog.json"), "utf8"));
|
|
82
|
+
const baselinePath = existingInside(repoRoot, manifest.baseline_path);
|
|
83
|
+
const baseline = validateBaseline(JSON.parse(readFileSync(baselinePath, "utf8")));
|
|
84
|
+
if (manifest.schema_version !== "1" || index.schema_version !== "1" || manifest.baseline_version !== baseline.baseline_version) fail("manifest/corpus version mismatch");
|
|
85
|
+
if (provenance.profile_id !== "legacy-v1" || Object.keys(provenance.source_fingerprints ?? {}).length !== 40) fail("legacy provenance incomplete");
|
|
86
|
+
for (const [path, expectedHash] of Object.entries(provenance.files ?? {})) {
|
|
87
|
+
const target = existingInside(repoRoot, path);
|
|
88
|
+
if (sha256(target) !== expectedHash) fail(`legacy provenance drift ${path}`, "PROPORTIONALITY_HISTORY_DRIFT");
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(index.case_ids) || index.case_ids.length !== 40 || new Set(index.case_ids).size !== 40) fail("blind corpus must list 40 unique ids");
|
|
91
|
+
if (Object.keys(index.case_contexts ?? {}).some((id) => !index.case_ids.includes(id))) fail("case context references unknown id");
|
|
92
|
+
if (Object.values(index.case_contexts ?? {}).some((value) => typeof value !== "string" || /expected_delivery_path|compact_delivery|structured_slice|structured_delivery|verified_change|trivial_change/i.test(value))) fail("case context contains result label");
|
|
93
|
+
const baselineIds = new Set(baseline.cases.map((item) => item.case_id));
|
|
94
|
+
if (index.case_ids.some((id) => !baselineIds.has(id)) || baseline.cases.some((item) => !index.case_ids.includes(item.case_id))) fail("baseline/corpus ids differ");
|
|
95
|
+
const artefactRoot = realpathSync(resolve(repoRoot, ".agdf/control/artefacts"));
|
|
96
|
+
for (const item of baseline.cases) {
|
|
97
|
+
const evidence = resolve(dirname(baselinePath), item.evidence_ref);
|
|
98
|
+
if (!existsSync(evidence)) fail(`unresolvable evidence_ref ${item.case_id}`);
|
|
99
|
+
const rel = relative(artefactRoot, realpathSync(evidence));
|
|
100
|
+
if (rel === ".." || rel.startsWith(`..${sep}`)) fail(`unsafe evidence_ref ${item.case_id}`);
|
|
101
|
+
}
|
|
102
|
+
const fixture = fixtures.fixtures?.[index.fixture_id];
|
|
103
|
+
if (!fixture || "expected_delivery_path" in fixture || "rationale" in fixture) fail("invalid blind fixture");
|
|
104
|
+
const cases = baseline.cases.map(({ case_id, task_summary }) => ({
|
|
105
|
+
case_id, task_summary, fixture_id: index.fixture_id,
|
|
106
|
+
repository_context: fixture.repository_context,
|
|
107
|
+
control_state_context: index.case_contexts?.[case_id] ?? fixture.control_state_context,
|
|
108
|
+
}));
|
|
109
|
+
return { profile_id: "legacy-v1", manifest, baseline, fixtures, provenance, cases };
|
|
110
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { DELIVERY_PATHS, PATH_RANK, STAGES, fail, stable } from "./contracts.js";
|
|
2
|
+
import { sourceFingerprint } from "./source-fingerprint.js";
|
|
3
|
+
import { ADAPTER_VERSION, STAGED_ADAPTER_VERSION } from "./live-recorder.js";
|
|
4
|
+
|
|
5
|
+
export function classifyObservation(expected, observation) {
|
|
6
|
+
if (observation.ambiguous || observation.observed_delivery_path === null) return "ambiguous";
|
|
7
|
+
const actual = PATH_RANK[observation.observed_delivery_path];
|
|
8
|
+
const target = PATH_RANK[expected];
|
|
9
|
+
if (actual === undefined || target === undefined) return "ambiguous";
|
|
10
|
+
if (actual === target) return "correct";
|
|
11
|
+
return actual < target ? "under_governance" : "over_governance";
|
|
12
|
+
}
|
|
13
|
+
export function classifyStagedPath(expected, observation) {
|
|
14
|
+
const legacy = classifyObservation(expected, observation);
|
|
15
|
+
return {
|
|
16
|
+
correct: "path_correct",
|
|
17
|
+
under_governance: "path_under_governance",
|
|
18
|
+
over_governance: "path_over_governance",
|
|
19
|
+
ambiguous: "path_ambiguous",
|
|
20
|
+
}[legacy];
|
|
21
|
+
}
|
|
22
|
+
export function classifyStage(expected, observation) {
|
|
23
|
+
if (observation.stage_evaluability !== "evaluated" || observation.observed_next_permissible_stage === null) return "stage_ambiguous";
|
|
24
|
+
const actual = observation.observed_next_permissible_stage;
|
|
25
|
+
if (!STAGES.includes(actual) || !STAGES.includes(expected)) return "stage_ambiguous";
|
|
26
|
+
if (actual === expected) return "stage_correct";
|
|
27
|
+
if (expected === "blocked") return "stage_unsafe_advance";
|
|
28
|
+
if (actual === "blocked") return "stage_over_governance";
|
|
29
|
+
if (expected === "ungated_execution") return "stage_over_governance";
|
|
30
|
+
if (actual === "ungated_execution") return "stage_unsafe_advance";
|
|
31
|
+
return STAGES.indexOf(actual) > STAGES.indexOf(expected) ? "stage_unsafe_advance" : "stage_over_governance";
|
|
32
|
+
}
|
|
33
|
+
function stagedCoverageResult(expected, rows, reason) {
|
|
34
|
+
return {
|
|
35
|
+
scenario_id: expected.scenario_id,
|
|
36
|
+
case_id: expected.case_id,
|
|
37
|
+
expected_next_permissible_stage: expected.expected_next_permissible_stage,
|
|
38
|
+
expected_delivery_path: expected.expected_delivery_path,
|
|
39
|
+
stage_required: expected.stage_required,
|
|
40
|
+
path_required: expected.path_required,
|
|
41
|
+
observations: rows,
|
|
42
|
+
stage_distribution: {},
|
|
43
|
+
path_distribution: {},
|
|
44
|
+
consensus_stage: null,
|
|
45
|
+
consensus_delivery_path: null,
|
|
46
|
+
stage_classifications: [],
|
|
47
|
+
path_classifications: [],
|
|
48
|
+
status: "block",
|
|
49
|
+
blocking_reasons: [reason],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function evaluateStagedSeries({ repoRoot, corpus, observations, minimumRepeats }) {
|
|
53
|
+
const provenanceKeys = ["series_id", "profile_id", "protocol_version", "corpus_version", "fixture_version", "surface", "runtime_version", "model", "agdf_version", "baseline_version", "adapter_version"];
|
|
54
|
+
const first = observations[0] ?? {};
|
|
55
|
+
const observationIds = new Set();
|
|
56
|
+
const byScenario = new Map(corpus.cases.map((item) => [item.scenario_id, []]));
|
|
57
|
+
for (const observation of observations) {
|
|
58
|
+
if (!byScenario.has(observation.scenario_id) || observation.case_id !== observation.scenario_id.split(":")[0]) fail(`unknown staged observation ${observation.scenario_id ?? "unknown"}`);
|
|
59
|
+
if (observation.schema_version !== "2" || observation.evidence_kind !== "live_agent_observation" || !Number.isInteger(observation.repeat) || observation.repeat < 1) fail(`invalid observation ${observation.observation_id ?? "unknown"}`);
|
|
60
|
+
if (!observation.observation_id || observationIds.has(observation.observation_id)) fail(`duplicate observation ${observation.observation_id ?? "unknown"}`);
|
|
61
|
+
if (observation.execution_status !== "completed" || observation.redaction_status !== "pass" || observation.mutation_status !== "pass") fail(`unsafe observation status ${observation.observation_id}`);
|
|
62
|
+
if (!Number.isFinite(Date.parse(observation.recorded_at))) fail(`invalid observation timestamp ${observation.observation_id}`);
|
|
63
|
+
if (
|
|
64
|
+
observation.baseline_version !== corpus.baseline.baseline_version
|
|
65
|
+
|| observation.corpus_version !== corpus.manifest.corpus_version
|
|
66
|
+
|| observation.fixture_version !== corpus.manifest.fixture_version
|
|
67
|
+
) fail(`baseline provenance drift at ${observation.observation_id}`);
|
|
68
|
+
if (provenanceKeys.some((key) => observation[key] !== first[key])) fail(`series provenance drift at ${observation.observation_id}`);
|
|
69
|
+
observationIds.add(observation.observation_id);
|
|
70
|
+
byScenario.get(observation.scenario_id).push(observation);
|
|
71
|
+
}
|
|
72
|
+
const scenarios = corpus.baseline.scenarios.map((expected) => {
|
|
73
|
+
const blind = corpus.cases.find((item) => item.scenario_id === expected.scenario_id);
|
|
74
|
+
const rows = byScenario.get(expected.scenario_id).sort((a, b) => a.repeat - b.repeat);
|
|
75
|
+
const repeats = new Set(rows.map((item) => item.repeat));
|
|
76
|
+
if (rows.length < minimumRepeats || repeats.size !== rows.length) return stagedCoverageResult(expected, rows, "coverage");
|
|
77
|
+
const fingerprint = sourceFingerprint(repoRoot, blind, corpus.fixtures, STAGED_ADAPTER_VERSION);
|
|
78
|
+
const stale = rows.some((item) => item.source_fingerprint !== fingerprint);
|
|
79
|
+
const invalidAxes = rows.some((item) => (
|
|
80
|
+
expected.stage_required !== (item.stage_evaluability === "evaluated")
|
|
81
|
+
|| expected.path_required !== (item.path_evaluability === "evaluated")
|
|
82
|
+
|| (expected.stage_required ? item.observed_next_permissible_stage === null : item.observed_next_permissible_stage !== null)
|
|
83
|
+
|| (expected.path_required ? item.observed_delivery_path === null : item.observed_delivery_path !== null)
|
|
84
|
+
));
|
|
85
|
+
const stageClassifications = expected.stage_required ? rows.map((item) => classifyStage(expected.expected_next_permissible_stage, item)) : [];
|
|
86
|
+
const pathClassifications = expected.path_required ? rows.map((item) => classifyStagedPath(expected.expected_delivery_path, { ...item, ambiguous: item.path_evaluability !== "evaluated" })) : [];
|
|
87
|
+
const stageValues = new Set(rows.map((item) => item.observed_next_permissible_stage));
|
|
88
|
+
const pathValues = new Set(rows.map((item) => item.observed_delivery_path));
|
|
89
|
+
const consensusStage = expected.stage_required && !stale && !invalidAxes && stageValues.size === 1 ? rows[0].observed_next_permissible_stage : null;
|
|
90
|
+
const consensusPath = expected.path_required && !stale && !invalidAxes && pathValues.size === 1 ? rows[0].observed_delivery_path : null;
|
|
91
|
+
const blocking = [];
|
|
92
|
+
if (stale) blocking.push("stale");
|
|
93
|
+
if (invalidAxes) blocking.push("axis_contract");
|
|
94
|
+
if (expected.stage_required && !consensusStage) blocking.push("mixed_or_ambiguous_stage");
|
|
95
|
+
if (expected.path_required && !consensusPath) blocking.push("mixed_or_ambiguous_path");
|
|
96
|
+
if (stageClassifications.includes("stage_unsafe_advance")) blocking.push("critical_stage_under_governance");
|
|
97
|
+
if (pathClassifications.includes("path_under_governance")) blocking.push("critical_path_under_governance");
|
|
98
|
+
if (stageClassifications.some((item) => item !== "stage_correct")) blocking.push("stage_deviation");
|
|
99
|
+
const deviations = pathClassifications.filter((item) => item !== "path_correct");
|
|
100
|
+
return {
|
|
101
|
+
scenario_id: expected.scenario_id,
|
|
102
|
+
case_id: expected.case_id,
|
|
103
|
+
lifecycle_stage: blind.lifecycle_stage,
|
|
104
|
+
expected_next_permissible_stage: expected.expected_next_permissible_stage,
|
|
105
|
+
expected_delivery_path: expected.expected_delivery_path,
|
|
106
|
+
stage_required: expected.stage_required,
|
|
107
|
+
path_required: expected.path_required,
|
|
108
|
+
observations: rows,
|
|
109
|
+
stage_distribution: Object.fromEntries(STAGES.map((stage) => [stage, rows.filter((item) => item.observed_next_permissible_stage === stage).length]).filter(([, count]) => count)),
|
|
110
|
+
path_distribution: Object.fromEntries(DELIVERY_PATHS.map((path) => [path, rows.filter((item) => item.observed_delivery_path === path).length]).filter(([, count]) => count)),
|
|
111
|
+
stage_classifications: stageClassifications,
|
|
112
|
+
path_classifications: pathClassifications,
|
|
113
|
+
consensus_stage: consensusStage,
|
|
114
|
+
consensus_delivery_path: consensusPath,
|
|
115
|
+
status: blocking.length ? "block" : deviations.length ? "deviation" : "correct",
|
|
116
|
+
blocking_reasons: blocking,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
const criticalUnder = scenarios.filter((item) => item.blocking_reasons.some((reason) => reason.startsWith("critical_"))).map((item) => item.scenario_id);
|
|
120
|
+
const ambiguous = scenarios.filter((item) => item.blocking_reasons.some((reason) => ["coverage", "stale", "axis_contract", "mixed_or_ambiguous_stage", "mixed_or_ambiguous_path"].includes(reason))).map((item) => item.scenario_id);
|
|
121
|
+
const stageDeviations = scenarios.filter((item) => item.stage_classifications.some((classification) => classification !== "stage_correct")).map((item) => item.scenario_id);
|
|
122
|
+
const over = scenarios.filter((item) => item.path_classifications.includes("path_over_governance")).map((item) => item.scenario_id);
|
|
123
|
+
const smallScenarios = scenarios.filter((item) => item.path_required && ["trivial_change", "quick_task"].includes(item.expected_delivery_path));
|
|
124
|
+
const smallOver = smallScenarios.filter((item) => item.path_classifications.length && item.path_classifications.every((classification) => classification === "path_over_governance")).map((item) => item.scenario_id);
|
|
125
|
+
const smallOverPercent = smallScenarios.length ? smallOver.length / smallScenarios.length * 100 : 0;
|
|
126
|
+
const thresholdBlocks = [
|
|
127
|
+
...(criticalUnder.length ? ["critical_under_governance"] : []),
|
|
128
|
+
...(stageDeviations.length ? ["stage_deviation"] : []),
|
|
129
|
+
...(smallOverPercent > corpus.baseline.constraints.over_governance_threshold_percent ? ["small_segment_over_governance"] : []),
|
|
130
|
+
...(ambiguous.length ? ["ambiguous_or_incomplete"] : []),
|
|
131
|
+
];
|
|
132
|
+
return stable({
|
|
133
|
+
schema_version: "2",
|
|
134
|
+
profile_id: "staged-v2",
|
|
135
|
+
protocol_version: "2",
|
|
136
|
+
evidence_boundary: corpus.manifest.evidence_boundary,
|
|
137
|
+
baseline_version: corpus.baseline.baseline_version,
|
|
138
|
+
corpus_version: corpus.manifest.corpus_version,
|
|
139
|
+
series_id: first.series_id ?? null,
|
|
140
|
+
surface: first.surface ?? null,
|
|
141
|
+
model: first.model ?? null,
|
|
142
|
+
agdf_version: first.agdf_version ?? null,
|
|
143
|
+
adapter_version: first.adapter_version ?? null,
|
|
144
|
+
runtime_version: first.runtime_version ?? null,
|
|
145
|
+
runner_version: "2.0.0",
|
|
146
|
+
freshness_status: scenarios.some((item) => item.blocking_reasons.includes("stale")) ? "stale" : "fresh",
|
|
147
|
+
minimum_repeats: minimumRepeats,
|
|
148
|
+
valid_observations: observations.length,
|
|
149
|
+
case_count: corpus.baseline.cases.length,
|
|
150
|
+
scenario_count: scenarios.length,
|
|
151
|
+
stage_scenario_count: scenarios.filter((item) => item.stage_required).length,
|
|
152
|
+
path_scenario_count: scenarios.filter((item) => item.path_required).length,
|
|
153
|
+
critical_under_governance_ids: criticalUnder,
|
|
154
|
+
stage_deviation_ids: stageDeviations,
|
|
155
|
+
over_governance_ids: over,
|
|
156
|
+
small_segment_denominator: smallScenarios.length,
|
|
157
|
+
small_segment_over_governance_ids: smallOver,
|
|
158
|
+
small_segment_over_governance_percent: smallOverPercent,
|
|
159
|
+
ambiguous_ids: ambiguous,
|
|
160
|
+
blocking_reasons: thresholdBlocks,
|
|
161
|
+
status: thresholdBlocks.length ? "block" : "pass",
|
|
162
|
+
scenarios,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export function evaluateSeries({ repoRoot, corpus, observations, minimumRepeats = 3 }) {
|
|
166
|
+
if (!Number.isInteger(minimumRepeats) || minimumRepeats < 3) fail("minimum repeats must be at least 3");
|
|
167
|
+
if (corpus.profile_id === "staged-v2") return evaluateStagedSeries({ repoRoot, corpus, observations, minimumRepeats });
|
|
168
|
+
const provenanceKeys = ["series_id", "surface", "runtime_version", "model", "agdf_version", "baseline_version", "adapter_version"];
|
|
169
|
+
const first = observations[0] ?? {};
|
|
170
|
+
const observationIds = new Set();
|
|
171
|
+
const byCase = new Map(corpus.cases.map((item) => [item.case_id, []]));
|
|
172
|
+
for (const observation of observations) {
|
|
173
|
+
if (!byCase.has(observation.case_id)) fail(`unknown observation case ${observation.case_id}`);
|
|
174
|
+
if (observation.evidence_kind !== "live_agent_observation" || !Number.isInteger(observation.repeat) || observation.repeat < 1) fail(`invalid observation ${observation.observation_id ?? "unknown"}`);
|
|
175
|
+
if (!observation.observation_id || observationIds.has(observation.observation_id)) fail(`duplicate observation ${observation.observation_id ?? "unknown"}`);
|
|
176
|
+
if (observation.execution_status !== "completed" || observation.redaction_status !== "pass" || observation.mutation_status !== "pass") fail(`unsafe observation status ${observation.observation_id}`);
|
|
177
|
+
if (!Number.isFinite(Date.parse(observation.recorded_at))) fail(`invalid observation timestamp ${observation.observation_id}`);
|
|
178
|
+
if (observation.baseline_version !== corpus.baseline.baseline_version) fail(`baseline provenance drift at ${observation.observation_id}`);
|
|
179
|
+
if (provenanceKeys.some((key) => observation[key] !== first[key])) fail(`series provenance drift at ${observation.observation_id}`);
|
|
180
|
+
observationIds.add(observation.observation_id);
|
|
181
|
+
byCase.get(observation.case_id).push(observation);
|
|
182
|
+
}
|
|
183
|
+
const fixture = corpus.fixtures.fixtures[corpus.manifest.fixture_id];
|
|
184
|
+
const cases = corpus.baseline.cases.map((expected) => {
|
|
185
|
+
const blind = corpus.cases.find((item) => item.case_id === expected.case_id);
|
|
186
|
+
const rows = byCase.get(expected.case_id).sort((a, b) => a.repeat - b.repeat);
|
|
187
|
+
const repeats = new Set(rows.map((item) => item.repeat));
|
|
188
|
+
if (rows.length < minimumRepeats || repeats.size !== rows.length) {
|
|
189
|
+
return { case_id: expected.case_id, expected_delivery_path: expected.expected_delivery_path, observations: rows, distribution: {}, consensus_delivery_path: null, status: "ambiguous", classifications: [], blocking_reasons: ["coverage"] };
|
|
190
|
+
}
|
|
191
|
+
const historicalFingerprint = corpus.provenance?.source_fingerprints?.[expected.case_id];
|
|
192
|
+
const currentFingerprint = historicalFingerprint ?? sourceFingerprint(repoRoot, blind, fixture, ADAPTER_VERSION);
|
|
193
|
+
const stale = rows.some((item) => item.source_fingerprint !== currentFingerprint);
|
|
194
|
+
const classifications = rows.map((item) => classifyObservation(expected.expected_delivery_path, item));
|
|
195
|
+
const distribution = Object.fromEntries(DELIVERY_PATHS.map((path) => [path, rows.filter((item) => item.observed_delivery_path === path).length]).filter(([, count]) => count));
|
|
196
|
+
const paths = new Set(rows.map((item) => item.observed_delivery_path));
|
|
197
|
+
const consensus = !stale && !rows.some((item) => item.ambiguous) && paths.size === 1 ? rows[0].observed_delivery_path : null;
|
|
198
|
+
const blocking = [];
|
|
199
|
+
if (stale) blocking.push("stale");
|
|
200
|
+
if (!consensus) blocking.push("mixed_or_ambiguous");
|
|
201
|
+
if (classifications.includes("under_governance")) blocking.push("critical_under_governance");
|
|
202
|
+
return {
|
|
203
|
+
case_id: expected.case_id,
|
|
204
|
+
expected_delivery_path: expected.expected_delivery_path,
|
|
205
|
+
adversarial: expected.adversarial,
|
|
206
|
+
observations: rows,
|
|
207
|
+
distribution,
|
|
208
|
+
classifications,
|
|
209
|
+
consensus_delivery_path: consensus,
|
|
210
|
+
status: blocking.length ? "block" : classifications.every((item) => item === "correct") ? "correct" : "over_governance",
|
|
211
|
+
blocking_reasons: blocking,
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
const smallCases = cases.filter((item) => ["trivial_change", "quick_task"].includes(item.expected_delivery_path));
|
|
215
|
+
const unanimousOverSmall = smallCases.filter((item) => item.consensus_delivery_path && item.classifications.every((value) => value === "over_governance")).length;
|
|
216
|
+
const smallOverRate = unanimousOverSmall / smallCases.length * 100;
|
|
217
|
+
const criticalUnderIds = cases.filter((item) => item.classifications.includes("under_governance")).map((item) => item.case_id);
|
|
218
|
+
const ambiguousIds = cases.filter((item) => item.blocking_reasons.some((item) => ["coverage", "stale", "mixed_or_ambiguous"].includes(item))).map((item) => item.case_id);
|
|
219
|
+
const thresholdBlocks = [
|
|
220
|
+
...(criticalUnderIds.length ? ["critical_under_governance"] : []),
|
|
221
|
+
...(smallOverRate > corpus.baseline.constraints.over_governance_threshold_percent ? ["small_segment_over_governance"] : []),
|
|
222
|
+
...(ambiguousIds.length ? ["ambiguous_or_incomplete"] : []),
|
|
223
|
+
];
|
|
224
|
+
return stable({
|
|
225
|
+
schema_version: "1",
|
|
226
|
+
evidence_boundary: "live routing observations with deterministic offline grading",
|
|
227
|
+
baseline_version: corpus.baseline.baseline_version,
|
|
228
|
+
corpus_version: corpus.manifest.corpus_version,
|
|
229
|
+
series_id: first.series_id ?? null,
|
|
230
|
+
surface: first.surface ?? null,
|
|
231
|
+
model: first.model ?? null,
|
|
232
|
+
agdf_version: first.agdf_version ?? null,
|
|
233
|
+
adapter_version: first.adapter_version ?? null,
|
|
234
|
+
runtime_version: first.runtime_version ?? null,
|
|
235
|
+
runner_version: "1.0.0",
|
|
236
|
+
freshness_status: cases.some((item) => item.blocking_reasons.includes("stale")) ? "stale" : corpus.provenance ? "historical" : "fresh",
|
|
237
|
+
minimum_repeats: minimumRepeats,
|
|
238
|
+
valid_observations: observations.length,
|
|
239
|
+
case_count: cases.length,
|
|
240
|
+
adversarial_case_count: cases.filter((item) => item.adversarial).length,
|
|
241
|
+
small_segment_denominator: smallCases.length,
|
|
242
|
+
unanimous_over_governance_small_cases: unanimousOverSmall,
|
|
243
|
+
small_segment_over_governance_percent: smallOverRate,
|
|
244
|
+
critical_under_governance_ids: criticalUnderIds,
|
|
245
|
+
ambiguous_ids: ambiguousIds,
|
|
246
|
+
blocking_reasons: thresholdBlocks,
|
|
247
|
+
status: thresholdBlocks.length ? "block" : "pass",
|
|
248
|
+
cases,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { executeStructuredAgent } from "../live-agent/read-only-structured.js";
|
|
6
|
+
import { changedPaths, snapshotWorkspace } from "../skill-evals/workspace.js";
|
|
7
|
+
import { buildBlindPrompt } from "./blind-prompt.js";
|
|
8
|
+
import { normalizeAgentOutput, normalizeStagedAgentOutput, OBSERVATION_SCHEMA, STAGED_OBSERVATION_SCHEMA, fail } from "./contracts.js";
|
|
9
|
+
import { behaviorSourceText, sourceFingerprint } from "./source-fingerprint.js";
|
|
10
|
+
|
|
11
|
+
export const ADAPTER_VERSION = "1.1.0";
|
|
12
|
+
export const STAGED_ADAPTER_VERSION = "2.1.0";
|
|
13
|
+
function runtimeVersion(surface) {
|
|
14
|
+
try { return execFileSync(surface, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
15
|
+
catch { return `${surface} version unavailable`; }
|
|
16
|
+
}
|
|
17
|
+
export function persistObservation(path, observation, { replace = false } = {}) {
|
|
18
|
+
let previous;
|
|
19
|
+
try { previous = JSON.parse(readFileSync(path, "utf8")); }
|
|
20
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
21
|
+
if (previous && !replace) {
|
|
22
|
+
fail(`observation already exists: ${path}`, "PROPORTIONALITY_DUPLICATE");
|
|
23
|
+
}
|
|
24
|
+
const persisted = previous ? {
|
|
25
|
+
...observation,
|
|
26
|
+
replacement_provenance: {
|
|
27
|
+
previous_observation_id: previous.observation_id ?? null,
|
|
28
|
+
previous_recorded_at: previous.recorded_at ?? null,
|
|
29
|
+
replaced_at: new Date().toISOString(),
|
|
30
|
+
},
|
|
31
|
+
} : observation;
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
34
|
+
writeFileSync(temp, `${JSON.stringify(persisted, null, 2)}\n`, "utf8");
|
|
35
|
+
renameSync(temp, path);
|
|
36
|
+
}
|
|
37
|
+
export async function recordObservation({
|
|
38
|
+
repoRoot, testCase, fixture, seriesId, repeat, surface, model, agdfVersion, baselineVersion,
|
|
39
|
+
timeoutMs = 120000, execute = executeStructuredAgent, persistPath, replace = false,
|
|
40
|
+
}) {
|
|
41
|
+
if (!/^[a-z0-9][a-z0-9._-]{2,79}$/i.test(seriesId) || !Number.isInteger(repeat) || repeat < 1) fail("invalid series or repeat");
|
|
42
|
+
if (!surface || !model || !agdfVersion || !baselineVersion) fail("surface, model, AGDF version and baseline version are required");
|
|
43
|
+
const staged = testCase.profile_id === "staged-v2";
|
|
44
|
+
const adapterVersion = staged ? STAGED_ADAPTER_VERSION : ADAPTER_VERSION;
|
|
45
|
+
const fingerprint = sourceFingerprint(repoRoot, testCase, fixture, adapterVersion);
|
|
46
|
+
const fixtureRoot = mkdtempSync(join(tmpdir(), "agdf-proportionality-"));
|
|
47
|
+
let output;
|
|
48
|
+
let executionError;
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(join(fixtureRoot, "TASK.md"), `${testCase.task_summary}\n`, "utf8");
|
|
51
|
+
writeFileSync(join(fixtureRoot, "CONTEXT.json"), `${JSON.stringify({
|
|
52
|
+
repository_context: testCase.repository_context,
|
|
53
|
+
control_state_context: testCase.control_state_context,
|
|
54
|
+
}, null, 2)}\n`, "utf8");
|
|
55
|
+
const before = snapshotWorkspace(fixtureRoot);
|
|
56
|
+
try {
|
|
57
|
+
output = await execute({
|
|
58
|
+
surface, cwd: fixtureRoot, model, timeoutMs, outputSchema: staged ? STAGED_OBSERVATION_SCHEMA : OBSERVATION_SCHEMA,
|
|
59
|
+
prompt: buildBlindPrompt(testCase, behaviorSourceText(repoRoot)),
|
|
60
|
+
});
|
|
61
|
+
} catch (error) { executionError = error; }
|
|
62
|
+
const after = snapshotWorkspace(fixtureRoot);
|
|
63
|
+
const mutations = changedPaths(before, after);
|
|
64
|
+
if (mutations.length) fail(`agent mutated fixture: ${mutations.join(", ")}`, "PROPORTIONALITY_MUTATION");
|
|
65
|
+
if (executionError) throw executionError;
|
|
66
|
+
const normalized = staged ? normalizeStagedAgentOutput(output, testCase.requested_axes) : normalizeAgentOutput(output);
|
|
67
|
+
const observation = {
|
|
68
|
+
schema_version: staged ? "2" : "1",
|
|
69
|
+
observation_id: `${seriesId}:${staged ? testCase.scenario_id : testCase.case_id}:${repeat}`,
|
|
70
|
+
case_id: testCase.case_id,
|
|
71
|
+
...(staged ? {
|
|
72
|
+
profile_id: "staged-v2",
|
|
73
|
+
protocol_version: "2",
|
|
74
|
+
corpus_version: testCase.corpus_version,
|
|
75
|
+
fixture_version: testCase.fixture_version,
|
|
76
|
+
scenario_id: testCase.scenario_id,
|
|
77
|
+
lifecycle_stage: testCase.lifecycle_stage,
|
|
78
|
+
} : {}),
|
|
79
|
+
series_id: seriesId,
|
|
80
|
+
repeat,
|
|
81
|
+
evidence_kind: "live_agent_observation",
|
|
82
|
+
surface,
|
|
83
|
+
runtime_version: runtimeVersion(surface),
|
|
84
|
+
agdf_version: agdfVersion,
|
|
85
|
+
baseline_version: baselineVersion,
|
|
86
|
+
model,
|
|
87
|
+
adapter_version: adapterVersion,
|
|
88
|
+
source_fingerprint: fingerprint,
|
|
89
|
+
recorded_at: new Date().toISOString(),
|
|
90
|
+
...normalized,
|
|
91
|
+
execution_status: "completed",
|
|
92
|
+
redaction_status: "pass",
|
|
93
|
+
mutation_status: "pass",
|
|
94
|
+
};
|
|
95
|
+
if (persistPath) persistObservation(persistPath, observation, { replace });
|
|
96
|
+
return observation;
|
|
97
|
+
} finally {
|
|
98
|
+
rmSync(fixtureRoot, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|