create-agdf 0.11.4 → 0.13.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 +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/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/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 +2 -2
- package/generated/plugins/agdf/.codex-plugin/plugin.json +9 -6
- 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 +41 -5
- package/generated/plugins/agdf/meta/agdf-runtime-contract.md +1 -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 +41 -5
- package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-policy.js +14 -7
- package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +85 -0
- package/generated/plugins/agdf/runtime/create-agdf/package.json +4 -65
- package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
- package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +146 -40
- package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +26 -0
- package/generated/plugins/agdf/skills/gate-check/SKILL.md +40 -18
- package/generated/plugins/agdf/submission/openai/availability.md +16 -0
- package/generated/plugins/agdf/submission/openai/capability-matrix.json +49 -0
- package/generated/plugins/agdf/submission/openai/release-notes.md +16 -0
- package/generated/plugins/agdf/submission/openai/reviewer-cases.json +96 -0
- package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +43 -0
- package/generated/submissions/openai/agdf/assets/agdf-icon.svg +16 -0
- package/generated/submissions/openai/agdf/assets/agdf-logo.svg +16 -0
- package/generated/submissions/openai/agdf/meta/agdf-agent-router.md +129 -0
- package/generated/submissions/openai/agdf/meta/agdf-constitution.md +329 -0
- package/generated/submissions/openai/agdf/meta/agdf-interaction-locales.json +514 -0
- package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +263 -0
- package/generated/submissions/openai/agdf/meta/agdf-runtime-contract.md +16 -0
- package/generated/submissions/openai/agdf/meta/agdf-tenets.md +201 -0
- package/generated/submissions/openai/agdf/meta/contracts/closeout.md +29 -0
- package/generated/submissions/openai/agdf/meta/contracts/context-graph.md +48 -0
- package/generated/submissions/openai/agdf/meta/contracts/control-scaffold.md +115 -0
- package/generated/submissions/openai/agdf/meta/contracts/gate-transition.md +165 -0
- package/generated/submissions/openai/agdf/meta/contracts/interaction.md +476 -0
- package/generated/submissions/openai/agdf/meta/contracts/modes.md +183 -0
- package/generated/submissions/openai/agdf/meta/contracts/quality.md +157 -0
- package/generated/submissions/openai/agdf/meta/contracts/task-target-resolution.md +102 -0
- package/generated/submissions/openai/agdf/skills/brownfield-analysis/SKILL.md +175 -0
- package/generated/submissions/openai/agdf/skills/brownfield-analysis/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/clean-implementation-review/SKILL.md +126 -0
- package/generated/submissions/openai/agdf/skills/clean-implementation-review/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/code-review/SKILL.md +119 -0
- package/generated/submissions/openai/agdf/skills/code-review/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/delivery-closeout/SKILL.md +118 -0
- package/generated/submissions/openai/agdf/skills/delivery-closeout/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/delivery-path-search/SKILL.md +94 -0
- package/generated/submissions/openai/agdf/skills/delivery-path-search/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/gate-check/SKILL.md +238 -0
- package/generated/submissions/openai/agdf/skills/gate-check/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/qa-gate/SKILL.md +137 -0
- package/generated/submissions/openai/agdf/skills/qa-gate/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/release-or/SKILL.md +137 -0
- package/generated/submissions/openai/agdf/skills/release-or/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/task-plan-review/SKILL.md +157 -0
- package/generated/submissions/openai/agdf/skills/task-plan-review/help.md +23 -0
- package/generated/submissions/openai/agdf/skills/ux-intent-definition/SKILL.md +94 -0
- package/generated/submissions/openai/agdf/skills/ux-intent-definition/help.md +17 -0
- package/generated/submissions/openai/agdf/submission/openai/availability.md +16 -0
- package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +49 -0
- package/generated/submissions/openai/agdf/submission/openai/inventory.json +175 -0
- package/generated/submissions/openai/agdf/submission/openai/listing.json +38 -0
- package/generated/submissions/openai/agdf/submission/openai/readiness.json +40 -0
- package/generated/submissions/openai/agdf/submission/openai/readiness.md +24 -0
- package/generated/submissions/openai/agdf/submission/openai/release-notes.md +16 -0
- package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +96 -0
- package/lib/cli/application.js +13 -2
- package/lib/control-evaluation/gate-policy.js +14 -7
- package/lib/installers/local-marketplace.js +1 -1
- package/lib/installers/opencode.js +128 -1
- package/lib/interaction-presentation.js +85 -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/public-plugin/builder.js +82 -0
- package/lib/public-plugin/contract.js +102 -0
- package/lib/public-plugin/manifest.js +72 -0
- package/lib/public-plugin/report.js +33 -0
- package/lib/public-plugin/validator.js +101 -0
- package/lib/skill-evals/live-recorder.js +3 -26
- package/opencode-plugin.js +40 -1
- package/package.json +7 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function renderMarkdown(report) {
|
|
2
|
+
if (report.profile_id === "staged-v2") {
|
|
3
|
+
const lines = [
|
|
4
|
+
"# AGDF Staged Proportionality Observation",
|
|
5
|
+
"",
|
|
6
|
+
`- Status: \`${report.status}\``,
|
|
7
|
+
`- Evidenzgrenze: ${report.evidence_boundary}`,
|
|
8
|
+
`- Profil/Protokoll: \`${report.profile_id}\` / \`${report.protocol_version}\``,
|
|
9
|
+
`- Serie: \`${report.series_id ?? "none"}\``,
|
|
10
|
+
`- Surface/Modell: \`${report.surface ?? "none"}\` / \`${report.model ?? "none"}\``,
|
|
11
|
+
`- Runtime/AGDF/Adapter/Runner: \`${report.runtime_version ?? "none"}\` / \`${report.agdf_version ?? "none"}\` / \`${report.adapter_version ?? "none"}\` / \`${report.runner_version}\``,
|
|
12
|
+
`- Freshness: \`${report.freshness_status}\``,
|
|
13
|
+
`- Beobachtungen: ${report.valid_observations}`,
|
|
14
|
+
`- Coverage: ${report.scenarios.filter((item) => !item.blocking_reasons.includes("coverage")).length}/${report.scenario_count} Szenarien aus ${report.case_count} Fällen`,
|
|
15
|
+
`- Stage-/Pfad-Szenarien: ${report.stage_scenario_count}/${report.path_scenario_count}`,
|
|
16
|
+
`- Kritische Under-Governance: ${report.critical_under_governance_ids.length}`,
|
|
17
|
+
`- Stage-Abweichungen: ${report.stage_deviation_ids.length}`,
|
|
18
|
+
`- Pfad-Over-Governance: ${report.over_governance_ids.length}`,
|
|
19
|
+
`- Small-Segment Over-Governance: ${report.small_segment_over_governance_ids.length}/${report.small_segment_denominator} (${report.small_segment_over_governance_percent} %)`,
|
|
20
|
+
`- Ambiguous/Stale/Incomplete: ${report.ambiguous_ids.length}`,
|
|
21
|
+
"",
|
|
22
|
+
"| Szenario | Lifecycle | Soll-Stage | Stage-Konsens | Soll-Pfad | Pfad-Konsens | Status |",
|
|
23
|
+
"|---|---|---|---|---|---|---|",
|
|
24
|
+
];
|
|
25
|
+
for (const item of report.scenarios) lines.push(`| ${item.scenario_id} | ${item.lifecycle_stage} | ${item.expected_next_permissible_stage ?? "n/a"} | ${item.consensus_stage ?? "n/a"} | ${item.expected_delivery_path ?? "n/a"} | ${item.consensus_delivery_path ?? "n/a"} | ${item.status} |`);
|
|
26
|
+
return `${lines.join("\n")}\n`;
|
|
27
|
+
}
|
|
28
|
+
const lines = [
|
|
29
|
+
"# AGDF Proportionality Benchmark",
|
|
30
|
+
"",
|
|
31
|
+
`- Status: \`${report.status}\``,
|
|
32
|
+
`- Evidenzgrenze: ${report.evidence_boundary}`,
|
|
33
|
+
`- Serie: \`${report.series_id ?? "none"}\``,
|
|
34
|
+
`- Surface/Modell: \`${report.surface ?? "none"}\` / \`${report.model ?? "none"}\``,
|
|
35
|
+
`- Runtime/AGDF/Adapter/Runner: \`${report.runtime_version ?? "none"}\` / \`${report.agdf_version ?? "none"}\` / \`${report.adapter_version ?? "none"}\` / \`${report.runner_version}\``,
|
|
36
|
+
`- Freshness: \`${report.freshness_status}\``,
|
|
37
|
+
`- Beobachtungen: ${report.valid_observations}`,
|
|
38
|
+
`- Coverage: ${report.cases.filter((item) => !item.blocking_reasons.includes("coverage")).length}/${report.case_count}`,
|
|
39
|
+
`- Kritische Under-Governance: ${report.critical_under_governance_ids.length}`,
|
|
40
|
+
`- Small-Segment Over-Governance: ${report.unanimous_over_governance_small_cases}/${report.small_segment_denominator} (${report.small_segment_over_governance_percent} %)`,
|
|
41
|
+
`- Ambiguous/Stale/Incomplete: ${report.ambiguous_ids.length}`,
|
|
42
|
+
"",
|
|
43
|
+
"| Case | Soll | Verteilung | Konsens | Status |",
|
|
44
|
+
"|---|---|---|---|---|",
|
|
45
|
+
];
|
|
46
|
+
for (const item of report.cases) {
|
|
47
|
+
const distribution = Object.entries(item.distribution).map(([path, count]) => `${path}:${count}`).join(", ") || "none";
|
|
48
|
+
lines.push(`| ${item.case_id} | ${item.expected_delivery_path} | ${distribution} | ${item.consensus_delivery_path ?? "none"} | ${item.status} |`);
|
|
49
|
+
}
|
|
50
|
+
return `${lines.join("\n")}\n`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { stable } from "./contracts.js";
|
|
5
|
+
|
|
6
|
+
export const BEHAVIOR_SOURCES = Object.freeze([
|
|
7
|
+
"plugin/meta/agdf-agent-router.md",
|
|
8
|
+
"plugin/meta/contracts/modes.md",
|
|
9
|
+
"plugin/meta/contracts/gate-transition.md",
|
|
10
|
+
"plugin/meta/contracts/interaction.md",
|
|
11
|
+
"plugin/skills/gate-check/SKILL.md",
|
|
12
|
+
]);
|
|
13
|
+
export const IMPLEMENTATION_SOURCES = Object.freeze([
|
|
14
|
+
"create-agdf/lib/live-agent/read-only-structured.js",
|
|
15
|
+
"create-agdf/lib/proportionality-benchmark/blind-prompt.js",
|
|
16
|
+
"create-agdf/lib/proportionality-benchmark/contracts.js",
|
|
17
|
+
"create-agdf/lib/proportionality-benchmark/corpus-loader.js",
|
|
18
|
+
"create-agdf/lib/proportionality-benchmark/evaluator.js",
|
|
19
|
+
"create-agdf/lib/proportionality-benchmark/live-recorder.js",
|
|
20
|
+
"create-agdf/lib/proportionality-benchmark/source-fingerprint.js",
|
|
21
|
+
]);
|
|
22
|
+
export function behaviorSourceText(repoRoot) {
|
|
23
|
+
return BEHAVIOR_SOURCES.map((path) => `--- ${path} ---\n${readFileSync(join(repoRoot, path), "utf8")}`).join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
export function sourceFingerprint(repoRoot, testCase, fixture, adapterVersion) {
|
|
26
|
+
const hash = createHash("sha256");
|
|
27
|
+
hash.update(JSON.stringify(stable({ testCase, fixture, adapterVersion })));
|
|
28
|
+
for (const path of [...BEHAVIOR_SOURCES, ...IMPLEMENTATION_SOURCES]) hash.update(path).update(readFileSync(join(repoRoot, path)));
|
|
29
|
+
return hash.digest("hex");
|
|
30
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { assertPublicPluginContract, loadJson } from "./contract.js";
|
|
5
|
+
import { renderCodexPluginManifest } from "./manifest.js";
|
|
6
|
+
import { createReadinessReport, renderReadinessReport } from "./report.js";
|
|
7
|
+
import { inventory, validateCandidate } from "./validator.js";
|
|
8
|
+
|
|
9
|
+
function write(path, content) {
|
|
10
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
11
|
+
writeFileSync(path, content, "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function json(value) {
|
|
15
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildPublicPluginCandidate({ repoRoot, outputRoot }) {
|
|
19
|
+
repoRoot = resolve(repoRoot);
|
|
20
|
+
outputRoot = resolve(outputRoot);
|
|
21
|
+
const expectedParent = resolve(repoRoot, "create-agdf", "generated", "submissions", "openai");
|
|
22
|
+
if (dirname(outputRoot) !== expectedParent) throw new Error(`Public candidate output must be a direct child of ${expectedParent}`);
|
|
23
|
+
mkdirSync(expectedParent, { recursive: true });
|
|
24
|
+
const temporaryRoot = mkdtempSync(join(expectedParent, ".agdf-build-"));
|
|
25
|
+
try {
|
|
26
|
+
const pluginRoot = join(repoRoot, "plugin");
|
|
27
|
+
const definition = loadJson(join(pluginRoot, "meta", "agdf-plugin.definition.json"));
|
|
28
|
+
const capabilityMatrix = loadJson(join(pluginRoot, "submission", "openai", "capability-matrix.json"));
|
|
29
|
+
const reviewerCases = loadJson(join(pluginRoot, "submission", "openai", "reviewer-cases.json"));
|
|
30
|
+
const releaseNotes = readFileSync(join(pluginRoot, "submission", "openai", "release-notes.md"), "utf8");
|
|
31
|
+
assertPublicPluginContract({ definition, capabilityMatrix, reviewerCases, releaseNotes });
|
|
32
|
+
|
|
33
|
+
write(join(temporaryRoot, ".codex-plugin", "plugin.json"), renderCodexPluginManifest(definition, { publicCandidate: true }));
|
|
34
|
+
for (const directory of ["skills", "meta", "assets"]) cpSync(join(pluginRoot, directory), join(temporaryRoot, directory), { recursive: true });
|
|
35
|
+
cpSync(join(pluginRoot, "submission", "openai"), join(temporaryRoot, "submission", "openai"), { recursive: true });
|
|
36
|
+
const listing = {
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
releaseVersion: definition.version,
|
|
39
|
+
submissionType: definition.publicDistribution.submissionType,
|
|
40
|
+
identity: {
|
|
41
|
+
technicalId: definition.publicDistribution.technicalId,
|
|
42
|
+
publicDisplayName: definition.publicDistribution.publicDisplayName,
|
|
43
|
+
fullDisplayName: definition.publicDistribution.fullDisplayName,
|
|
44
|
+
},
|
|
45
|
+
interface: {
|
|
46
|
+
shortDescription: definition.publicDistribution.shortDescription,
|
|
47
|
+
longDescription: definition.longDescription,
|
|
48
|
+
developerName: definition.publicDistribution.developerName,
|
|
49
|
+
category: definition.publicDistribution.category,
|
|
50
|
+
defaultPrompt: definition.publicDistribution.defaultPrompt,
|
|
51
|
+
},
|
|
52
|
+
publisher: definition.publicDistribution.publisher,
|
|
53
|
+
urls: definition.publicDistribution.urls,
|
|
54
|
+
availability: definition.publicDistribution.availability,
|
|
55
|
+
};
|
|
56
|
+
write(join(temporaryRoot, "submission", "openai", "listing.json"), json(listing));
|
|
57
|
+
validateCandidate(temporaryRoot);
|
|
58
|
+
const entries = inventory(temporaryRoot, { exclude: ["submission/openai/inventory.json", "submission/openai/readiness.json"] });
|
|
59
|
+
const digest = createHash("sha256").update(json(entries)).digest("hex");
|
|
60
|
+
write(join(temporaryRoot, "submission", "openai", "inventory.json"), json({ schemaVersion: 1, releaseVersion: definition.version, digest, files: entries }));
|
|
61
|
+
const report = createReadinessReport({ definition, fileCount: entries.length, inventoryDigest: digest });
|
|
62
|
+
write(join(temporaryRoot, "submission", "openai", "readiness.json"), json(report));
|
|
63
|
+
write(join(temporaryRoot, "submission", "openai", "readiness.md"), renderReadinessReport(report));
|
|
64
|
+
validateCandidate(temporaryRoot);
|
|
65
|
+
|
|
66
|
+
const backupRoot = `${outputRoot}.previous`;
|
|
67
|
+
if (existsSync(backupRoot)) throw new Error(`Refusing candidate swap while recovery backup exists: ${backupRoot}`);
|
|
68
|
+
const hadPrevious = existsSync(outputRoot);
|
|
69
|
+
if (hadPrevious) renameSync(outputRoot, backupRoot);
|
|
70
|
+
try {
|
|
71
|
+
renameSync(temporaryRoot, outputRoot);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (hadPrevious && existsSync(backupRoot) && !existsSync(outputRoot)) renameSync(backupRoot, outputRoot);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (hadPrevious) rmSync(backupRoot, { recursive: true, force: true });
|
|
77
|
+
return { outputRoot, digest, fileCount: entries.length, report };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const LISTING_LIMITS = Object.freeze({
|
|
4
|
+
displayName: 30,
|
|
5
|
+
shortDescription: 30,
|
|
6
|
+
developerName: 80,
|
|
7
|
+
promptCount: 3,
|
|
8
|
+
promptLength: 128,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function unicodeLength(value) {
|
|
12
|
+
return [...String(value)].length;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requiredString(value, label, errors) {
|
|
16
|
+
if (typeof value !== "string" || value.trim() === "") errors.push(`${label} is required`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function within(value, limit, label, errors) {
|
|
20
|
+
requiredString(value, label, errors);
|
|
21
|
+
if (typeof value === "string" && unicodeLength(value) > limit) {
|
|
22
|
+
errors.push(`${label} exceeds ${limit} Unicode code points`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadJson(path) {
|
|
27
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validatePublicPluginContract({ definition, capabilityMatrix, reviewerCases, releaseNotes }) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
const contract = definition?.publicDistribution;
|
|
33
|
+
if (contract?.schemaVersion !== 1) errors.push("publicDistribution.schemaVersion must be 1");
|
|
34
|
+
if (contract?.submissionType !== "skills") errors.push("submissionType must be skills");
|
|
35
|
+
if (contract?.technicalId !== definition?.id) errors.push("technicalId must match canonical plugin id");
|
|
36
|
+
if (contract?.developerName !== definition?.developerName) errors.push("developerName must match canonical developerName");
|
|
37
|
+
if (contract?.category !== definition?.category) errors.push("category must match canonical category");
|
|
38
|
+
within(contract?.publicDisplayName, LISTING_LIMITS.displayName, "publicDisplayName", errors);
|
|
39
|
+
within(contract?.shortDescription, LISTING_LIMITS.shortDescription, "shortDescription", errors);
|
|
40
|
+
within(contract?.developerName, LISTING_LIMITS.developerName, "developerName", errors);
|
|
41
|
+
requiredString(contract?.fullDisplayName, "fullDisplayName", errors);
|
|
42
|
+
requiredString(definition?.longDescription, "longDescription", errors);
|
|
43
|
+
if (Object.hasOwn(contract ?? {}, "longDescription")) {
|
|
44
|
+
errors.push("publicDistribution.longDescription must use canonical longDescription");
|
|
45
|
+
}
|
|
46
|
+
if (contract?.fullDisplayName !== `${definition?.displayName} (${contract?.publicDisplayName})`) {
|
|
47
|
+
errors.push("fullDisplayName must combine canonical displayName and publicDisplayName");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const prompts = contract?.defaultPrompt;
|
|
51
|
+
if (!Array.isArray(prompts) || prompts.length !== LISTING_LIMITS.promptCount) {
|
|
52
|
+
errors.push(`defaultPrompt must contain exactly ${LISTING_LIMITS.promptCount} prompts`);
|
|
53
|
+
} else {
|
|
54
|
+
prompts.forEach((prompt, index) => within(prompt, LISTING_LIMITS.promptLength, `defaultPrompt[${index}]`, errors));
|
|
55
|
+
}
|
|
56
|
+
for (const [name, value] of Object.entries(contract?.urls ?? {})) {
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(value);
|
|
59
|
+
if (url.protocol !== "https:") errors.push(`${name} URL must use HTTPS`);
|
|
60
|
+
} catch {
|
|
61
|
+
errors.push(`${name} URL is invalid`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const name of ["website", "privacy", "terms", "support"]) {
|
|
65
|
+
if (!contract?.urls?.[name]) errors.push(`${name} URL is required`);
|
|
66
|
+
}
|
|
67
|
+
if (contract?.publisher?.verificationState !== "unverified") {
|
|
68
|
+
errors.push("publisher verificationState must remain unverified without portal evidence");
|
|
69
|
+
}
|
|
70
|
+
if (!contract?.availability || !Array.isArray(contract.availability.countries) || !Array.isArray(contract.availability.regions) || !Array.isArray(contract.availability.workspaceContexts)) {
|
|
71
|
+
errors.push("availability decision record is incomplete");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const allowedStates = new Set(["common", "codex-specific", "chatgpt-specific", "advisory", "unavailable", "unverified"]);
|
|
75
|
+
if (capabilityMatrix?.releaseVersion !== definition?.version) errors.push("capability matrix version must match definition");
|
|
76
|
+
if (!Array.isArray(capabilityMatrix?.capabilities) || capabilityMatrix.capabilities.length === 0) errors.push("capability matrix is empty");
|
|
77
|
+
for (const capability of capabilityMatrix?.capabilities ?? []) {
|
|
78
|
+
if (!allowedStates.has(capability.state)) errors.push(`capability ${capability.id ?? "unknown"} has invalid state`);
|
|
79
|
+
requiredString(capability.evidenceClass, `capability ${capability.id} evidenceClass`, errors);
|
|
80
|
+
requiredString(capability.evidenceRef, `capability ${capability.id} evidenceRef`, errors);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (reviewerCases?.releaseVersion !== definition?.version) errors.push("reviewer cases version must match definition");
|
|
84
|
+
const cases = reviewerCases?.cases ?? [];
|
|
85
|
+
if (cases.filter((entry) => entry.kind === "positive").length < 5) errors.push("at least five positive reviewer cases are required");
|
|
86
|
+
if (cases.filter((entry) => entry.kind === "negative").length < 3) errors.push("at least three negative reviewer cases are required");
|
|
87
|
+
for (const entry of cases) {
|
|
88
|
+
for (const field of ["id", "kind", "theme", "prompt", "expectedWorkflow", "expectedResult"]) requiredString(entry[field], `${entry.id ?? "case"}.${field}`, errors);
|
|
89
|
+
if (!Array.isArray(entry.prerequisites)) errors.push(`${entry.id}.prerequisites must be an array`);
|
|
90
|
+
}
|
|
91
|
+
for (const theme of ["missing-approval", "disproportionate-fit", "unavailable-repository-authority", "unsupported-host-capability", "unauthorized-external-action"]) {
|
|
92
|
+
if (!cases.some((entry) => entry.kind === "negative" && entry.theme === theme)) errors.push(`negative reviewer theme ${theme} is required`);
|
|
93
|
+
}
|
|
94
|
+
requiredString(releaseNotes, "releaseNotes", errors);
|
|
95
|
+
if (typeof releaseNotes === "string" && !releaseNotes.includes(definition?.version)) errors.push("release notes must contain exact version");
|
|
96
|
+
return errors;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function assertPublicPluginContract(inputs) {
|
|
100
|
+
const errors = validatePublicPluginContract(inputs);
|
|
101
|
+
if (errors.length) throw new Error(`AGDF_PUBLIC_PLUGIN_CONTRACT_INVALID: ${errors.join("; ")}`);
|
|
102
|
+
}
|