@agent-delivery-harness/conformance 0.1.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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +159 -0
- package/fixtures/repo-config-adapter.ts +250 -0
- package/package.json +24 -0
- package/src/generate.ts +1013 -0
- package/src/index.ts +36 -0
- package/src/json-schema.ts +222 -0
- package/src/runner.ts +687 -0
- package/vectors/context/environment.json +21 -0
- package/vectors/context/repo-config.json +22 -0
- package/vectors/kit.json +1250 -0
- package/vectors/vectors/accept/a-completed-order-differs.json +112 -0
- package/vectors/vectors/accept/a-cost-fractional.json +116 -0
- package/vectors/vectors/accept/a-cost-tokens.json +120 -0
- package/vectors/vectors/accept/a-deferred-expansion.json +124 -0
- package/vectors/vectors/accept/a-idempotent-resubmission.json +115 -0
- package/vectors/vectors/accept/a-minimal-green.json +97 -0
- package/vectors/vectors/accept/a-resolved-and-advisory.json +129 -0
- package/vectors/vectors/accept/a-three-reviewers.json +113 -0
- package/vectors/vectors/reject/env-1-unregistered-provider.json +114 -0
- package/vectors/vectors/reject/env-10-artifact-duplicate.json +103 -0
- package/vectors/vectors/reject/env-10-artifact-missing-file.json +104 -0
- package/vectors/vectors/reject/env-10-artifact-traversal.json +99 -0
- package/vectors/vectors/reject/env-11-artifact-digest-mismatch.json +98 -0
- package/vectors/vectors/reject/env-12-unknown-attestation-level.json +114 -0
- package/vectors/vectors/reject/env-13-nonempty-signatures.json +118 -0
- package/vectors/vectors/reject/env-14-duplicate-claim.json +149 -0
- package/vectors/vectors/reject/env-14-no-claims.json +78 -0
- package/vectors/vectors/reject/env-14-unconfigured-obligation.json +114 -0
- package/vectors/vectors/reject/env-14-unsupported-payload-spec.json +114 -0
- package/vectors/vectors/reject/env-2-run-id-1.json +114 -0
- package/vectors/vectors/reject/env-2-run-id-2.json +114 -0
- package/vectors/vectors/reject/env-2-run-id-3.json +114 -0
- package/vectors/vectors/reject/env-2-run-id-4.json +114 -0
- package/vectors/vectors/reject/env-2-run-id-5.json +114 -0
- package/vectors/vectors/reject/env-4-unsupported-vcs.json +114 -0
- package/vectors/vectors/reject/env-5-malformed-object-id.json +114 -0
- package/vectors/vectors/reject/env-6-missing-identity.json +111 -0
- package/vectors/vectors/reject/env-6-unknown-identity-version.json +114 -0
- package/vectors/vectors/reject/env-8-repository-required.json +116 -0
- package/vectors/vectors/reject/env-9-empty-run-history.json +107 -0
- package/vectors/vectors/reject/env-9-final-pass-mismatch.json +110 -0
- package/vectors/vectors/reject/env-9-final-tree-mismatch.json +110 -0
- package/vectors/vectors/reject/gen-1-legacy-count-field.json +115 -0
- package/vectors/vectors/reject/gen-1-unknown-envelope-member.json +117 -0
- package/vectors/vectors/reject/gen-2-unsupported-spec.json +114 -0
- package/vectors/vectors/reject/rg-1-edited-after-final-pass.json +114 -0
- package/vectors/vectors/reject/rg-1-not-finalized.json +114 -0
- package/vectors/vectors/reject/rg-1-verdict-not-green.json +114 -0
- package/vectors/vectors/reject/rg-10-cost-by-reviewer-exceeds-total.json +115 -0
- package/vectors/vectors/reject/rg-10-cost-empty-reported-by.json +111 -0
- package/vectors/vectors/reject/rg-10-cost-empty-unit.json +111 -0
- package/vectors/vectors/reject/rg-10-cost-missing-unit.json +110 -0
- package/vectors/vectors/reject/rg-10-cost-negative-by-reviewer.json +114 -0
- package/vectors/vectors/reject/rg-10-cost-negative-total.json +111 -0
- package/vectors/vectors/reject/rg-10-cost-null-total.json +111 -0
- package/vectors/vectors/reject/rg-10-cost-unknown-reviewer-in-breakdown.json +114 -0
- package/vectors/vectors/reject/rg-2-duplicate-selected.json +99 -0
- package/vectors/vectors/reject/rg-3-completed-missing.json +96 -0
- package/vectors/vectors/reject/rg-3-completed-unrelated.json +99 -0
- package/vectors/vectors/reject/rg-3-failed-reviewer.json +100 -0
- package/vectors/vectors/reject/rg-3-timed-out-reviewer.json +100 -0
- package/vectors/vectors/reject/rg-4-approval-extra.json +104 -0
- package/vectors/vectors/reject/rg-4-approval-malformed.json +98 -0
- package/vectors/vectors/reject/rg-4-approval-missing.json +100 -0
- package/vectors/vectors/reject/rg-4-approval-not-approved.json +98 -0
- package/vectors/vectors/reject/rg-4-approval-wrong-candidate.json +98 -0
- package/vectors/vectors/reject/rg-4-approval-wrong-pass.json +98 -0
- package/vectors/vectors/reject/rg-4-approval-wrong-provider.json +98 -0
- package/vectors/vectors/reject/rg-4-approval-wrong-reviewer.json +98 -0
- package/vectors/vectors/reject/rg-4-approval-wrong-run.json +98 -0
- package/vectors/vectors/reject/rg-4-approval-wrong-workspace.json +98 -0
- package/vectors/vectors/reject/rg-5-duplicate-finding-id.json +131 -0
- package/vectors/vectors/reject/rg-5-unknown-disposition.json +123 -0
- package/vectors/vectors/reject/rg-5-unknown-scope.json +123 -0
- package/vectors/vectors/reject/rg-5-unknown-severity.json +123 -0
- package/vectors/vectors/reject/rg-6-actionable-ignored.json +123 -0
- package/vectors/vectors/reject/rg-6-actionable-unresolved.json +123 -0
- package/vectors/vectors/reject/rg-6-blocking-finding.json +123 -0
- package/vectors/vectors/reject/rg-7-defer-adjacent.json +126 -0
- package/vectors/vectors/reject/rg-7-defer-blocking.json +126 -0
- package/vectors/vectors/reject/rg-7-defer-in-contract.json +126 -0
- package/vectors/vectors/reject/rg-7-defer-lowercase-issue-id.json +126 -0
- package/vectors/vectors/reject/rg-7-defer-missing-issue-id.json +125 -0
- package/vectors/vectors/reject/rg-7-defer-non-actionable.json +126 -0
- package/vectors/vectors/reject/rg-7-defer-p0.json +126 -0
- package/vectors/vectors/reject/rg-7-defer-p1.json +126 -0
- package/vectors/vectors/reject/rg-7-defer-placeholder-issue-id.json +126 -0
- package/vectors/vectors/reject/rg-7-issue-id-on-resolved.json +124 -0
- package/vectors/vectors/reject/rg-8-counts-contradict-findings.json +126 -0
- package/vectors/vectors/reject/rg-8-deferral-count-disagrees.json +124 -0
- package/vectors/vectors/reject/rg-8-deferral-ids-disagree.json +126 -0
- package/vectors/vectors/reject/rg-9-iteration-count-mismatch.json +114 -0
- package/vectors/vectors/reject/sub-1-base-tip-moved.json +131 -0
- package/vectors/vectors/reject/sub-1-deliverable-changed.json +131 -0
- package/vectors/vectors/reject/sub-1-merge-base-moved.json +131 -0
- package/vectors/vectors/reject/sub-1-raw-tree-changed.json +131 -0
- package/vectors/vectors/reject/sub-2-unprepared.json +117 -0
- package/vectors/vectors/reject/sub-3-manifest-outside-run-root.json +117 -0
- package/vectors/vectors/reject/sub-4-record-conflict.json +220 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delivery-evidence/1 conformance kit.
|
|
3
|
+
*
|
|
4
|
+
* The vendored 89-vector kit lives in ./vectors (kit root: kit.json, context/,
|
|
5
|
+
* vectors/accept/, vectors/reject/) and is regenerated byte-identically by
|
|
6
|
+
* ./generate.ts. The runner below drives it in unit mode — the manifest-decidable
|
|
7
|
+
* vectors, judged by the pure kernel validator under a configuration the caller
|
|
8
|
+
* supplies. Integration mode, which adds the five recorder-dependent vectors,
|
|
9
|
+
* arrives with the recorder.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const PACKAGE_NAME = "@agent-delivery-harness/conformance";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
RECORDER_DEPENDENT_VECTORS,
|
|
16
|
+
compareOutcome,
|
|
17
|
+
describeFailures,
|
|
18
|
+
evaluateVector,
|
|
19
|
+
kitRoot,
|
|
20
|
+
loadKitEnvironment,
|
|
21
|
+
loadKitIndex,
|
|
22
|
+
loadKitVectors,
|
|
23
|
+
runKitUnitMode,
|
|
24
|
+
type DeferredVector,
|
|
25
|
+
type KitEnvironment,
|
|
26
|
+
type KitExpectation,
|
|
27
|
+
type KitIndex,
|
|
28
|
+
type KitIndexEntry,
|
|
29
|
+
type KitOutcome,
|
|
30
|
+
type KitOutcomeStatus,
|
|
31
|
+
type KitRunOptions,
|
|
32
|
+
type KitRunResult,
|
|
33
|
+
type KitVector,
|
|
34
|
+
type ValidatorOutcome,
|
|
35
|
+
} from "./runner.ts";
|
|
36
|
+
export { validateAgainstSchema, type SchemaViolation } from "./json-schema.ts";
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal JSON Schema (draft 2020-12) evaluator, for cross-checking the
|
|
3
|
+
* published schemas against the normative validator.
|
|
4
|
+
*
|
|
5
|
+
* WHY NOT A SCHEMA LIBRARY. The schemas are cross-checked, not relied on: the
|
|
6
|
+
* hand-rolled validator is the normative implementation, and this evaluator
|
|
7
|
+
* exists only to answer "does the published shape agree with the vectors?". A
|
|
8
|
+
* dependency on the most security-adjacent path in the repository is a poor
|
|
9
|
+
* trade for a job this small, and the same reasoning kept the canonicalizer
|
|
10
|
+
* in-kernel.
|
|
11
|
+
*
|
|
12
|
+
* WHY IT REFUSES WHAT IT DOES NOT KNOW. It implements exactly the keywords the
|
|
13
|
+
* two published schemas use and **throws** on any other keyword — and, for the
|
|
14
|
+
* same reason, on a constraint sitting beside a `$ref`, which it would otherwise
|
|
15
|
+
* drop while resolving. An evaluator that skipped an unrecognized constraint
|
|
16
|
+
* would silently report "passes the schema" for a document the schema rejects,
|
|
17
|
+
* which is the one failure this cross-check cannot afford: the partition it
|
|
18
|
+
* feeds would then be wrong in the direction that hides a disagreement.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface SchemaViolation {
|
|
22
|
+
/** RFC 6901 pointer into the instance. */
|
|
23
|
+
readonly pointer: string;
|
|
24
|
+
readonly keyword: string;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SUPPORTED_KEYWORDS: ReadonlySet<string> = new Set([
|
|
29
|
+
// Metadata, no constraint.
|
|
30
|
+
"$schema",
|
|
31
|
+
"$id",
|
|
32
|
+
"$defs",
|
|
33
|
+
"title",
|
|
34
|
+
"description",
|
|
35
|
+
"format",
|
|
36
|
+
// Constraints.
|
|
37
|
+
"$ref",
|
|
38
|
+
"type",
|
|
39
|
+
"const",
|
|
40
|
+
"enum",
|
|
41
|
+
"required",
|
|
42
|
+
"properties",
|
|
43
|
+
"additionalProperties",
|
|
44
|
+
"items",
|
|
45
|
+
"minItems",
|
|
46
|
+
"maxItems",
|
|
47
|
+
"uniqueItems",
|
|
48
|
+
"minLength",
|
|
49
|
+
"maxLength",
|
|
50
|
+
"pattern",
|
|
51
|
+
"minimum",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
type Schema = Record<string, unknown>;
|
|
55
|
+
|
|
56
|
+
/** A subschema is an object, or a boolean: `true` admits anything, `false` nothing. */
|
|
57
|
+
type Subschema = Schema | boolean;
|
|
58
|
+
|
|
59
|
+
/** Keywords that carry no constraint, and so may legally sit beside a `$ref`. */
|
|
60
|
+
const METADATA_KEYWORDS: ReadonlySet<string> = new Set(["$ref", "$schema", "$id", "$defs", "title", "description"]);
|
|
61
|
+
|
|
62
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
63
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function typeMatches(name: string, value: unknown): boolean {
|
|
67
|
+
switch (name) {
|
|
68
|
+
case "object":
|
|
69
|
+
return isRecord(value);
|
|
70
|
+
case "array":
|
|
71
|
+
return Array.isArray(value);
|
|
72
|
+
case "string":
|
|
73
|
+
return typeof value === "string";
|
|
74
|
+
case "number":
|
|
75
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
76
|
+
case "integer":
|
|
77
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
78
|
+
case "boolean":
|
|
79
|
+
return typeof value === "boolean";
|
|
80
|
+
case "null":
|
|
81
|
+
return value === null;
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`unsupported JSON Schema type "${name}"`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function escape(token: string): string {
|
|
88
|
+
return token.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function validateAgainstSchema(schema: unknown, instance: unknown): readonly SchemaViolation[] {
|
|
92
|
+
if (!isRecord(schema)) throw new Error("schema must be an object");
|
|
93
|
+
const violations: SchemaViolation[] = [];
|
|
94
|
+
check(schema, schema, instance, "", violations);
|
|
95
|
+
return violations;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolve(root: Schema, schema: Schema): Schema {
|
|
99
|
+
const ref = schema["$ref"];
|
|
100
|
+
if (ref === undefined) return schema;
|
|
101
|
+
if (typeof ref !== "string" || !ref.startsWith("#/$defs/")) {
|
|
102
|
+
throw new Error(`unsupported $ref "${String(ref)}"`);
|
|
103
|
+
}
|
|
104
|
+
// In 2020-12 a constraint beside a `$ref` still applies. This evaluator
|
|
105
|
+
// replaces the schema with its target, so a sibling constraint would be
|
|
106
|
+
// dropped — which is the silent-pass failure the whole module refuses.
|
|
107
|
+
const siblings = Object.keys(schema).filter((keyword) => !METADATA_KEYWORDS.has(keyword));
|
|
108
|
+
if (siblings.length > 0) {
|
|
109
|
+
throw new Error(`unsupported $ref sibling keyword(s): ${siblings.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
const defs = root["$defs"];
|
|
112
|
+
const name = ref.slice("#/$defs/".length);
|
|
113
|
+
if (!isRecord(defs) || !isRecord(defs[name])) throw new Error(`unresolvable $ref "${ref}"`);
|
|
114
|
+
return defs[name];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Evaluates a value against a subschema in either of its two forms. */
|
|
118
|
+
function checkSubschema(
|
|
119
|
+
root: Schema,
|
|
120
|
+
subschema: Subschema,
|
|
121
|
+
instance: unknown,
|
|
122
|
+
pointer: string,
|
|
123
|
+
violations: SchemaViolation[],
|
|
124
|
+
keyword: string,
|
|
125
|
+
message: string,
|
|
126
|
+
): void {
|
|
127
|
+
if (subschema === true) return;
|
|
128
|
+
if (subschema === false) {
|
|
129
|
+
violations.push({ pointer, keyword, message });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!isRecord(subschema)) throw new Error(`unsupported subschema ${JSON.stringify(subschema)}`);
|
|
133
|
+
check(root, subschema, instance, pointer, violations);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function check(root: Schema, rawSchema: Schema, instance: unknown, pointer: string, violations: SchemaViolation[]): void {
|
|
137
|
+
for (const keyword of Object.keys(rawSchema)) {
|
|
138
|
+
if (!SUPPORTED_KEYWORDS.has(keyword)) {
|
|
139
|
+
throw new Error(`unsupported JSON Schema keyword "${keyword}"`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const schema = resolve(root, rawSchema);
|
|
143
|
+
if (schema !== rawSchema) {
|
|
144
|
+
check(root, schema, instance, pointer, violations);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const emit = (keyword: string, message: string): void => {
|
|
149
|
+
violations.push({ pointer, keyword, message });
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const type = schema["type"];
|
|
153
|
+
if (type !== undefined) {
|
|
154
|
+
const names = Array.isArray(type) ? (type as string[]) : [type as string];
|
|
155
|
+
if (!names.some((name) => typeMatches(name, instance))) {
|
|
156
|
+
emit("type", `expected ${names.join(" or ")}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (Object.prototype.hasOwnProperty.call(schema, "const") && JSON.stringify(schema["const"]) !== JSON.stringify(instance)) {
|
|
162
|
+
emit("const", `expected ${JSON.stringify(schema["const"])}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const enumeration = schema["enum"];
|
|
166
|
+
if (Array.isArray(enumeration) && !enumeration.some((option) => JSON.stringify(option) === JSON.stringify(instance))) {
|
|
167
|
+
emit("enum", `expected one of ${JSON.stringify(enumeration)}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof instance === "string") {
|
|
171
|
+
const pattern = schema["pattern"];
|
|
172
|
+
if (typeof pattern === "string" && !new RegExp(pattern, "u").test(instance)) emit("pattern", `does not match ${pattern}`);
|
|
173
|
+
const minLength = schema["minLength"];
|
|
174
|
+
if (typeof minLength === "number" && instance.length < minLength) emit("minLength", `shorter than ${minLength}`);
|
|
175
|
+
const maxLength = schema["maxLength"];
|
|
176
|
+
if (typeof maxLength === "number" && instance.length > maxLength) emit("maxLength", `longer than ${maxLength}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof instance === "number") {
|
|
180
|
+
const minimum = schema["minimum"];
|
|
181
|
+
if (typeof minimum === "number" && instance < minimum) emit("minimum", `below ${minimum}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (Array.isArray(instance)) {
|
|
185
|
+
const minItems = schema["minItems"];
|
|
186
|
+
if (typeof minItems === "number" && instance.length < minItems) emit("minItems", `fewer than ${minItems} items`);
|
|
187
|
+
const maxItems = schema["maxItems"];
|
|
188
|
+
if (typeof maxItems === "number" && instance.length > maxItems) emit("maxItems", `more than ${maxItems} items`);
|
|
189
|
+
if (schema["uniqueItems"] === true) {
|
|
190
|
+
const seen = new Set(instance.map((item) => JSON.stringify(item)));
|
|
191
|
+
if (seen.size !== instance.length) emit("uniqueItems", "items are not unique");
|
|
192
|
+
}
|
|
193
|
+
const items = schema["items"];
|
|
194
|
+
if (items !== undefined) {
|
|
195
|
+
instance.forEach((item, index) => {
|
|
196
|
+
checkSubschema(root, items as Subschema, item, `${pointer}/${index}`, violations, "items", "no item is valid here");
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isRecord(instance)) {
|
|
202
|
+
const required = schema["required"];
|
|
203
|
+
if (Array.isArray(required)) {
|
|
204
|
+
for (const name of required as string[]) {
|
|
205
|
+
if (!Object.prototype.hasOwnProperty.call(instance, name)) {
|
|
206
|
+
emit("required", `missing required property "${name}"`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
211
|
+
for (const [name, value] of Object.entries(instance)) {
|
|
212
|
+
const at = `${pointer}/${escape(name)}`;
|
|
213
|
+
if (Object.prototype.hasOwnProperty.call(properties, name)) {
|
|
214
|
+
checkSubschema(root, properties[name] as Subschema, value, at, violations, "properties", "no value is valid for this property");
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const additional = schema["additionalProperties"];
|
|
218
|
+
if (additional === undefined) continue;
|
|
219
|
+
checkSubschema(root, additional as Subschema, value, at, violations, "additionalProperties", "property is not allowed");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|