@honua/mcp-server 0.0.20-alpha.0 → 0.1.0-beta.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 +92 -15
- package/certification/geospatial-mcp-schemas/PROVENANCE.md +15 -7
- package/certification/geospatial-mcp-schemas/conformance/manifest.schema.json +6 -0
- package/certification/geospatial-mcp-schemas/index.json +88 -9
- package/certification/geospatial-mcp-schemas/tools/apply_style_preset.schema.json +13 -9
- package/certification/geospatial-mcp-schemas/tools/buffer_features.schema.json +60 -0
- package/certification/geospatial-mcp-schemas/tools/edit_features.schema.json +74 -0
- package/certification/geospatial-mcp-schemas/tools/export_dataset.schema.json +60 -0
- package/certification/geospatial-mcp-schemas/tools/geocode_addresses.schema.json +34 -0
- package/certification/geospatial-mcp-schemas/tools/get_style.schema.json +13 -3
- package/certification/geospatial-mcp-schemas/tools/ingest_dataset.schema.json +45 -0
- package/certification/geospatial-mcp-schemas/tools/join_features.schema.json +73 -0
- package/certification/geospatial-mcp-schemas/tools/overlay_features.schema.json +53 -0
- package/certification/geospatial-mcp-schemas/tools/query_features.schema.json +20 -0
- package/certification/geospatial-mcp-schemas/tools/render_map.schema.json +5 -0
- package/certification/geospatial-mcp-schemas/tools/reproject_features.schema.json +49 -0
- package/certification/geospatial-mcp-schemas/tools/summarize_statistics.schema.json +71 -0
- package/dist/src/capability.d.ts +48 -0
- package/dist/src/capability.d.ts.map +1 -0
- package/dist/src/capability.js +44 -0
- package/dist/src/certification/census-data.d.ts +33 -0
- package/dist/src/certification/census-data.d.ts.map +1 -0
- package/dist/src/certification/census-data.js +506 -0
- package/dist/src/certification/census-fixture-client.d.ts +7 -0
- package/dist/src/certification/census-fixture-client.d.ts.map +1 -0
- package/dist/src/certification/census-fixture-client.js +226 -0
- package/dist/src/certification/certifier.d.ts +7 -1
- package/dist/src/certification/certifier.d.ts.map +1 -1
- package/dist/src/certification/certifier.js +52 -7
- package/dist/src/certification/cli.js +19 -4
- package/dist/src/certification/contracts.d.ts +41 -0
- package/dist/src/certification/contracts.d.ts.map +1 -1
- package/dist/src/certification/contracts.js +71 -0
- package/dist/src/certification/fixtures.d.ts +13 -0
- package/dist/src/certification/fixtures.d.ts.map +1 -1
- package/dist/src/certification/fixtures.js +28 -0
- package/dist/src/certification/json-schema.d.ts +23 -1
- package/dist/src/certification/json-schema.d.ts.map +1 -1
- package/dist/src/certification/json-schema.js +10 -2
- package/dist/src/certification/lifecycle.d.ts +14 -0
- package/dist/src/certification/lifecycle.d.ts.map +1 -0
- package/dist/src/certification/lifecycle.js +326 -0
- package/dist/src/certification/operator-catalog.d.ts +8 -0
- package/dist/src/certification/operator-catalog.d.ts.map +1 -1
- package/dist/src/certification/operator-catalog.js +189 -5
- package/dist/src/certification/report.d.ts.map +1 -1
- package/dist/src/certification/report.js +13 -0
- package/dist/src/certification/run.d.ts.map +1 -1
- package/dist/src/certification/run.js +1 -0
- package/dist/src/certification/schema-index.d.ts +25 -0
- package/dist/src/certification/schema-index.d.ts.map +1 -1
- package/dist/src/certification/schema-index.js +27 -0
- package/dist/src/certification/target.d.ts +9 -1
- package/dist/src/certification/target.d.ts.map +1 -1
- package/dist/src/certification/target.js +54 -2
- package/dist/src/eval/cli.js +4 -2
- package/dist/src/eval/corpus.d.ts +7 -0
- package/dist/src/eval/corpus.d.ts.map +1 -1
- package/dist/src/eval/corpus.js +16 -0
- package/dist/src/eval/drivers/deterministic.d.ts.map +1 -1
- package/dist/src/eval/drivers/deterministic.js +13 -0
- package/dist/src/eval/grade.d.ts.map +1 -1
- package/dist/src/eval/grade.js +20 -1
- package/dist/src/eval/report.d.ts +5 -0
- package/dist/src/eval/report.d.ts.map +1 -1
- package/dist/src/eval/report.js +3 -0
- package/dist/src/eval/runner.d.ts +6 -0
- package/dist/src/eval/runner.d.ts.map +1 -1
- package/dist/src/eval/runner.js +21 -7
- package/dist/src/eval/standalone-corpus.d.ts +3 -0
- package/dist/src/eval/standalone-corpus.d.ts.map +1 -0
- package/dist/src/eval/standalone-corpus.js +699 -0
- package/dist/src/eval/types.d.ts +25 -0
- package/dist/src/eval/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +29 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +41 -21
- package/dist/src/provenance.d.ts +44 -0
- package/dist/src/provenance.d.ts.map +1 -0
- package/dist/src/provenance.js +37 -0
- package/dist/src/resources/styles.d.ts.map +1 -1
- package/dist/src/resources/styles.js +36 -3
- package/dist/src/styles.d.ts +2 -0
- package/dist/src/styles.d.ts.map +1 -1
- package/dist/src/styles.js +29 -3
- package/dist/src/tools/apply-style-preset.d.ts.map +1 -1
- package/dist/src/tools/apply-style-preset.js +14 -2
- package/dist/src/tools/get-style.d.ts.map +1 -1
- package/dist/src/tools/get-style.js +24 -13
- package/dist/src/tools/list-services.d.ts.map +1 -1
- package/dist/src/tools/list-services.js +16 -1
- package/package.json +11 -2
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { CENSUS_EXTENT, CENSUS_FIELDS, CENSUS_LAYER_NAME, CENSUS_ROWS, CENSUS_SERVICE_ID, } from "./census-data.js";
|
|
2
|
+
/**
|
|
3
|
+
* Offline PLATFORM-FREE fixture `HonuaClient` (issue #369).
|
|
4
|
+
*
|
|
5
|
+
* Where {@link ./fixture-client.ts:createFixtureClient} models a Honua-enhanced
|
|
6
|
+
* deployment (it serves the OGC API – Styles surface), this client models a PLAIN
|
|
7
|
+
* public Esri FeatureServer — exactly what the standalone `honua-mcp` front door
|
|
8
|
+
* points at. It replays the recorded US Census apportionment layer
|
|
9
|
+
* (services.arcgis.com; see `census-data.ts`) through a tiny in-memory GeoServices
|
|
10
|
+
* evaluator so `count / query / extent / statistics` return REAL, deterministic
|
|
11
|
+
* answers with no network. Crucially it exposes NO Honua-only surface: every
|
|
12
|
+
* `pipelineFetch` (the OGC API – Styles path the style tools probe) 404s, so the
|
|
13
|
+
* capability-degradation path is exercised for real.
|
|
14
|
+
*
|
|
15
|
+
* The evaluator is verified against the live recordings in
|
|
16
|
+
* `test/certification/census-fixture-client.test.ts` (count=52, sum(pop)=335085841,
|
|
17
|
+
* sum(seats)=435, max=California, min=Wyoming), so the semantic eval assertions
|
|
18
|
+
* that ride on it are grounded, not hand-waved.
|
|
19
|
+
*/
|
|
20
|
+
const SPATIAL_REFERENCE = { wkid: 102100, latestWkid: 3857 };
|
|
21
|
+
function fieldValue(row, field) {
|
|
22
|
+
return row[field];
|
|
23
|
+
}
|
|
24
|
+
function compare(actual, op, expected) {
|
|
25
|
+
if (actual === undefined || actual === null) {
|
|
26
|
+
// NULL never satisfies a comparison (matches GeoServices/SQL semantics).
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (typeof expected === "string" || typeof actual === "string") {
|
|
30
|
+
const a = String(actual);
|
|
31
|
+
const e = String(expected);
|
|
32
|
+
switch (op) {
|
|
33
|
+
case "=":
|
|
34
|
+
return a === e;
|
|
35
|
+
case "<>":
|
|
36
|
+
case "!=":
|
|
37
|
+
return a !== e;
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`census fixture: operator ${op} is not supported for string values`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
switch (op) {
|
|
43
|
+
case "=":
|
|
44
|
+
return actual === expected;
|
|
45
|
+
case "<>":
|
|
46
|
+
case "!=":
|
|
47
|
+
return actual !== expected;
|
|
48
|
+
case ">":
|
|
49
|
+
return actual > expected;
|
|
50
|
+
case ">=":
|
|
51
|
+
return actual >= expected;
|
|
52
|
+
case "<":
|
|
53
|
+
return actual < expected;
|
|
54
|
+
case "<=":
|
|
55
|
+
return actual <= expected;
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`census fixture: unsupported operator "${op}"`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function matchClause(row, clause) {
|
|
61
|
+
const match = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|=|>|<)\s*(.+?)\s*$/.exec(clause);
|
|
62
|
+
if (!match) {
|
|
63
|
+
throw new Error(`census fixture: unsupported WHERE clause "${clause}"`);
|
|
64
|
+
}
|
|
65
|
+
const [, field, op, rawValue] = match;
|
|
66
|
+
const actual = fieldValue(row, field);
|
|
67
|
+
if (actual === undefined) {
|
|
68
|
+
throw new Error(`census fixture: unknown field "${field}" in WHERE`);
|
|
69
|
+
}
|
|
70
|
+
const expected = /^'.*'$/.test(rawValue) ? rawValue.slice(1, -1) : Number(rawValue);
|
|
71
|
+
if (typeof expected === "number" && Number.isNaN(expected)) {
|
|
72
|
+
throw new Error(`census fixture: unparseable value in WHERE clause "${clause}"`);
|
|
73
|
+
}
|
|
74
|
+
return compare(actual, op, expected);
|
|
75
|
+
}
|
|
76
|
+
/** Filter the recorded rows by a (minimal) SQL WHERE clause. */
|
|
77
|
+
export function filterRows(where) {
|
|
78
|
+
const clause = (where ?? "1=1").trim();
|
|
79
|
+
if (clause === "" || clause === "1=1") {
|
|
80
|
+
return [...CENSUS_ROWS];
|
|
81
|
+
}
|
|
82
|
+
const conditions = clause.split(/\s+AND\s+/i);
|
|
83
|
+
return CENSUS_ROWS.filter((row) => conditions.every((condition) => matchClause(row, condition)));
|
|
84
|
+
}
|
|
85
|
+
function orderRows(rows, orderByFields) {
|
|
86
|
+
const specs = orderByFields.split(",").map((part) => {
|
|
87
|
+
const [field, dir] = part.trim().split(/\s+/);
|
|
88
|
+
return { field, descending: (dir ?? "ASC").toUpperCase() === "DESC" };
|
|
89
|
+
});
|
|
90
|
+
return [...rows].sort((a, b) => {
|
|
91
|
+
for (const { field, descending } of specs) {
|
|
92
|
+
const av = fieldValue(a, field);
|
|
93
|
+
const bv = fieldValue(b, field);
|
|
94
|
+
if (av === bv) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const less = (av ?? 0) < (bv ?? 0);
|
|
98
|
+
return (less ? -1 : 1) * (descending ? -1 : 1);
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function aggregate(rows, type, field) {
|
|
104
|
+
// GeoServices aggregate statistics ignore NULL values (e.g. DC/PR seats).
|
|
105
|
+
const values = rows.map((row) => fieldValue(row, field)).filter((v) => typeof v === "number");
|
|
106
|
+
switch (type) {
|
|
107
|
+
case "count":
|
|
108
|
+
return values.length;
|
|
109
|
+
case "sum":
|
|
110
|
+
return values.reduce((acc, v) => acc + v, 0);
|
|
111
|
+
case "min":
|
|
112
|
+
return values.length > 0 ? Math.min(...values) : 0;
|
|
113
|
+
case "max":
|
|
114
|
+
return values.length > 0 ? Math.max(...values) : 0;
|
|
115
|
+
case "avg":
|
|
116
|
+
return values.reduce((acc, v) => acc + v, 0) / (values.length || 1);
|
|
117
|
+
case "stddev": {
|
|
118
|
+
const mean = values.reduce((acc, v) => acc + v, 0) / (values.length || 1);
|
|
119
|
+
const variance = values.reduce((acc, v) => acc + (v - mean) ** 2, 0) / (values.length || 1);
|
|
120
|
+
return Math.sqrt(variance);
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
throw new Error(`census fixture: unsupported statisticType "${type}"`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function computeStatistics(rows, request) {
|
|
127
|
+
const stats = request.outStatistics ?? [];
|
|
128
|
+
const groupField = request.groupByFieldsForStatistics?.split(",")[0]?.trim();
|
|
129
|
+
const build = (subset, groupValue) => {
|
|
130
|
+
const attributes = {};
|
|
131
|
+
if (groupField && groupValue !== undefined) {
|
|
132
|
+
attributes[groupField] = groupValue;
|
|
133
|
+
}
|
|
134
|
+
for (const stat of stats) {
|
|
135
|
+
attributes[stat.outStatisticFieldName] = aggregate(subset, stat.statisticType, stat.onStatisticField);
|
|
136
|
+
}
|
|
137
|
+
return { attributes };
|
|
138
|
+
};
|
|
139
|
+
if (!groupField) {
|
|
140
|
+
return { features: [build(rows)] };
|
|
141
|
+
}
|
|
142
|
+
const groups = new Map();
|
|
143
|
+
for (const row of rows) {
|
|
144
|
+
const key = fieldValue(row, groupField) ?? "";
|
|
145
|
+
const bucket = groups.get(key) ?? [];
|
|
146
|
+
bucket.push(row);
|
|
147
|
+
groups.set(key, bucket);
|
|
148
|
+
}
|
|
149
|
+
const features = [...groups.entries()].map(([value, subset]) => build(subset, value));
|
|
150
|
+
return { features };
|
|
151
|
+
}
|
|
152
|
+
function projectAttributes(row, outFields) {
|
|
153
|
+
if (outFields === undefined || outFields === "*" || (Array.isArray(outFields) && outFields.length === 0)) {
|
|
154
|
+
return { ...row };
|
|
155
|
+
}
|
|
156
|
+
const requested = Array.isArray(outFields) ? outFields : outFields.split(",").map((f) => f.trim());
|
|
157
|
+
const attributes = {};
|
|
158
|
+
for (const field of requested) {
|
|
159
|
+
const value = fieldValue(row, field);
|
|
160
|
+
if (value !== undefined) {
|
|
161
|
+
attributes[field] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return attributes;
|
|
165
|
+
}
|
|
166
|
+
/** Build the offline platform-free fixture client (a plain public FeatureServer). */
|
|
167
|
+
export function createStandaloneFixtureClient() {
|
|
168
|
+
const client = {
|
|
169
|
+
serverBaseUrl: "https://services.arcgis.com",
|
|
170
|
+
async listServices() {
|
|
171
|
+
return { services: [{ name: CENSUS_SERVICE_ID, type: "FeatureServer" }] };
|
|
172
|
+
},
|
|
173
|
+
async getFeatureServiceMetadata(_serviceId) {
|
|
174
|
+
return {
|
|
175
|
+
serviceDescription: "US Census 2020 apportionment (public Esri Living Atlas FeatureServer)",
|
|
176
|
+
layers: [{ id: 0, name: CENSUS_LAYER_NAME }],
|
|
177
|
+
spatialReference: SPATIAL_REFERENCE,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
async getLayerMetadata(_serviceId, _layerId) {
|
|
181
|
+
return {
|
|
182
|
+
id: 0,
|
|
183
|
+
name: CENSUS_LAYER_NAME,
|
|
184
|
+
description: "One row per state (plus DC and Puerto Rico) with 2020 population and apportioned House seats.",
|
|
185
|
+
geometryType: "esriGeometryPolygon",
|
|
186
|
+
fields: CENSUS_FIELDS.map((f) => ({ ...f })),
|
|
187
|
+
extent: CENSUS_EXTENT,
|
|
188
|
+
spatialReference: SPATIAL_REFERENCE,
|
|
189
|
+
relationships: [],
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
async queryFeatures(request = {}) {
|
|
193
|
+
const rows = filterRows(request.where);
|
|
194
|
+
const extra = request.extraParams ?? {};
|
|
195
|
+
if (extra.returnCountOnly === true) {
|
|
196
|
+
return { count: rows.length };
|
|
197
|
+
}
|
|
198
|
+
if (extra.returnExtentOnly === true) {
|
|
199
|
+
return { count: rows.length, extent: CENSUS_EXTENT };
|
|
200
|
+
}
|
|
201
|
+
if (request.outStatistics && request.outStatistics.length > 0) {
|
|
202
|
+
return computeStatistics(rows, request);
|
|
203
|
+
}
|
|
204
|
+
const ordered = request.orderByFields ? orderRows(rows, request.orderByFields) : rows;
|
|
205
|
+
const offset = request.resultOffset ?? 0;
|
|
206
|
+
const limit = request.resultRecordCount ?? ordered.length;
|
|
207
|
+
const page = ordered.slice(offset, offset + limit);
|
|
208
|
+
return {
|
|
209
|
+
objectIdFieldName: "OBJECTID",
|
|
210
|
+
geometryType: "esriGeometryPolygon",
|
|
211
|
+
spatialReference: SPATIAL_REFERENCE,
|
|
212
|
+
features: page.map((row) => ({ attributes: projectAttributes(row, request.outFields) })),
|
|
213
|
+
exceededTransferLimit: false,
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
// No Honua-only surface: the OGC API - Styles probe always 404s so the
|
|
217
|
+
// capability-degradation path is exercised against a real plain endpoint.
|
|
218
|
+
async pipelineFetch(_method, _path) {
|
|
219
|
+
return new Response(JSON.stringify({ error: { code: 404, message: "not found" } }), {
|
|
220
|
+
status: 404,
|
|
221
|
+
headers: { "Content-Type": "application/json" },
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
return client;
|
|
226
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { type AuthMode, type SuiteProvenance } from "../provenance.js";
|
|
2
3
|
import { AUTH_ERROR_CODES } from "./contracts.js";
|
|
3
4
|
import type { CertTargetMode } from "./target.js";
|
|
4
5
|
/** Per-tool certification record (machine-readable). */
|
|
@@ -37,7 +38,7 @@ export interface KnownGap {
|
|
|
37
38
|
}
|
|
38
39
|
/** A cross-cutting contract check against the certified surface. */
|
|
39
40
|
export interface ContractCheck {
|
|
40
|
-
contract: "output-schema" | "error-shape" | "auth-unauthenticated" | "list-pagination";
|
|
41
|
+
contract: "output-schema" | "error-shape" | "auth-unauthenticated" | "list-pagination" | "query-pagination" | "mutating-round-trip" | "mutating-permission-denied" | "async-job-lifecycle";
|
|
41
42
|
target: string;
|
|
42
43
|
status: "passed" | "failed" | "skipped";
|
|
43
44
|
detail: string;
|
|
@@ -61,6 +62,8 @@ export interface CertificationReport {
|
|
|
61
62
|
indexDate: string;
|
|
62
63
|
dialect: string;
|
|
63
64
|
};
|
|
65
|
+
/** Self-proving provenance: what was certified, with which suite, how authed. */
|
|
66
|
+
provenance: SuiteProvenance;
|
|
64
67
|
summary: {
|
|
65
68
|
pass: boolean;
|
|
66
69
|
toolsDiscovered: number;
|
|
@@ -74,6 +77,7 @@ export interface CertificationReport {
|
|
|
74
77
|
contractsChecked: number;
|
|
75
78
|
contractsPassed: number;
|
|
76
79
|
contractsFailed: number;
|
|
80
|
+
contractsSkipped: number;
|
|
77
81
|
knownGaps: number;
|
|
78
82
|
failures: number;
|
|
79
83
|
};
|
|
@@ -99,6 +103,8 @@ export interface CertifyOptions {
|
|
|
99
103
|
honuaTransport?: string | null;
|
|
100
104
|
/** Open a fresh, unauthenticated client for the auth contract (optional). */
|
|
101
105
|
connectUnauthenticated?: (() => Promise<Client>) | undefined;
|
|
106
|
+
/** How the authenticated client reached the surface (recorded in provenance). */
|
|
107
|
+
authMode?: AuthMode;
|
|
102
108
|
/** Environment used to resolve round-trip identifiers. */
|
|
103
109
|
env?: NodeJS.ProcessEnv;
|
|
104
110
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certifier.d.ts","sourceRoot":"","sources":["../../../src/certification/certifier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,EACL,gBAAgB,EAMjB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"certifier.d.ts","sourceRoot":"","sources":["../../../src/certification/certifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { readNegotiatedProtocolVersion, resolveSuiteGitSha, } from "../provenance.js";
|
|
1
2
|
import { AUTH_ERROR_CODES, checkAuthContract, checkInvalidArgsContract, checkPaginationContract, } from "./contracts.js";
|
|
2
3
|
import { buildInvalidArgsInputs, buildRoundTripInputs, resolveRoundTripEnv } from "./fixtures.js";
|
|
3
4
|
import { checkConformance, checkWellFormed, validateAgainstSchema } from "./json-schema.js";
|
|
4
|
-
import {
|
|
5
|
+
import { runDeepContracts } from "./lifecycle.js";
|
|
6
|
+
import { PLATFORM_FREE_DEGRADATIONS, buildReferenceToolLookup, loadSchemaFile, loadSchemaIndex, } from "./schema-index.js";
|
|
5
7
|
const STANDARD_SOURCE = "geospatial-mcp@54cbd49 (spec/schemas)";
|
|
6
8
|
/** Determine whether a discovered tool is safe to round-trip (read-only). */
|
|
7
9
|
export function isReadOnlyTool(tool) {
|
|
@@ -26,11 +28,15 @@ export async function certify(options) {
|
|
|
26
28
|
const { tools: advertisedTools, pages: toolPages } = await listAllTools(client);
|
|
27
29
|
const { resources: advertisedResources, pages: resourcePages } = await listAllResources(client);
|
|
28
30
|
const prompts = await listPromptsSafely(client);
|
|
31
|
+
// The platform-free standalone target has no Honua publishing surface, so
|
|
32
|
+
// reference-shape tools (e.g. apply_style_preset) intentionally degrade their
|
|
33
|
+
// platform-bound required properties. Conformance is evaluated accordingly.
|
|
34
|
+
const platformFree = options.targetMode === "standalone";
|
|
29
35
|
const tools = [];
|
|
30
36
|
const advertisedNames = new Set();
|
|
31
37
|
for (const tool of advertisedTools) {
|
|
32
38
|
advertisedNames.add(tool.name);
|
|
33
|
-
tools.push(await certifyTool(client, tool, referenceLookup, roundTripInputs));
|
|
39
|
+
tools.push(await certifyTool(client, tool, referenceLookup, roundTripInputs, platformFree));
|
|
34
40
|
}
|
|
35
41
|
const resources = advertisedResources.map((r) => ({
|
|
36
42
|
name: r.name,
|
|
@@ -41,9 +47,29 @@ export async function certify(options) {
|
|
|
41
47
|
contracts.push(paginationCheck("tools", toolPages));
|
|
42
48
|
contracts.push(paginationCheck("resources", resourcePages));
|
|
43
49
|
collectOutputSchemaContracts(tools, contracts);
|
|
44
|
-
contracts.push(await runErrorShapeContract(client, advertisedTools));
|
|
50
|
+
contracts.push(await runErrorShapeContract(client, advertisedTools, options.targetMode));
|
|
45
51
|
await runAuthContracts(options, advertisedTools, advertisedResources, contracts);
|
|
52
|
+
// Deep contracts: mutating round-trip, unauthenticated-write refusal, async job
|
|
53
|
+
// lifecycle, and query pagination. Safe by construction — mutations only run
|
|
54
|
+
// against the disposable offline mock or an explicit scratch target.
|
|
55
|
+
const deep = await runDeepContracts({
|
|
56
|
+
client,
|
|
57
|
+
advertisedToolNames: advertisedNames,
|
|
58
|
+
isDisposableBackend: options.backend === "mock-upstream",
|
|
59
|
+
connectUnauthenticated: options.connectUnauthenticated,
|
|
60
|
+
env: options.env,
|
|
61
|
+
});
|
|
62
|
+
contracts.push(...deep);
|
|
46
63
|
const knownGaps = collectKnownGaps(index, advertisedNames, referenceLookup);
|
|
64
|
+
const gitSha = resolveSuiteGitSha(options.env);
|
|
65
|
+
const provenance = {
|
|
66
|
+
suiteGitSha: gitSha.sha,
|
|
67
|
+
suiteGitShaSource: gitSha.source,
|
|
68
|
+
targetUrl: options.surface,
|
|
69
|
+
protocolVersion: readNegotiatedProtocolVersion(client),
|
|
70
|
+
toolCount: advertisedTools.length,
|
|
71
|
+
authMode: options.authMode ?? (options.backend === "mock-upstream" ? "bearer" : "unknown"),
|
|
72
|
+
};
|
|
47
73
|
return assembleReport({
|
|
48
74
|
serverInfo: serverInfo,
|
|
49
75
|
index,
|
|
@@ -53,6 +79,7 @@ export async function certify(options) {
|
|
|
53
79
|
prompts,
|
|
54
80
|
contracts,
|
|
55
81
|
knownGaps,
|
|
82
|
+
provenance,
|
|
56
83
|
});
|
|
57
84
|
}
|
|
58
85
|
async function listAllTools(client) {
|
|
@@ -88,7 +115,7 @@ async function listPromptsSafely(client) {
|
|
|
88
115
|
return [];
|
|
89
116
|
}
|
|
90
117
|
}
|
|
91
|
-
async function certifyTool(client, tool, referenceLookup, roundTripInputs) {
|
|
118
|
+
async function certifyTool(client, tool, referenceLookup, roundTripInputs, platformFree) {
|
|
92
119
|
const errors = [];
|
|
93
120
|
const notes = [];
|
|
94
121
|
const wellFormed = checkWellFormed(tool.inputSchema);
|
|
@@ -110,7 +137,8 @@ async function certifyTool(client, tool, referenceLookup, roundTripInputs) {
|
|
|
110
137
|
if (standardEntry) {
|
|
111
138
|
const standardSchema = loadSchemaFile(standardEntry.schema);
|
|
112
139
|
if (wellFormed.wellFormed) {
|
|
113
|
-
const
|
|
140
|
+
const shedRequired = platformFree ? PLATFORM_FREE_DEGRADATIONS[tool.name] : undefined;
|
|
141
|
+
const result = checkConformance(tool.inputSchema, standardSchema, { shedRequired });
|
|
114
142
|
conformant = result.conformant;
|
|
115
143
|
if (!result.conformant) {
|
|
116
144
|
errors.push(...result.violations.map((v) => `conformance: ${v}`));
|
|
@@ -212,7 +240,21 @@ function collectOutputSchemaContracts(tools, contracts) {
|
|
|
212
240
|
});
|
|
213
241
|
}
|
|
214
242
|
}
|
|
215
|
-
async function runErrorShapeContract(client, advertised) {
|
|
243
|
+
async function runErrorShapeContract(client, advertised, targetMode) {
|
|
244
|
+
// The platform-free standalone surface is built on the high-level MCP server,
|
|
245
|
+
// which rejects invalid arguments with a JSON-RPC -32602 (InvalidParams) error
|
|
246
|
+
// BEFORE the tool body runs — a valid MCP-native structured error, but not the
|
|
247
|
+
// Honua GeoprocessingError envelope this contract asserts. That envelope is a
|
|
248
|
+
// Honua-operator richness a plain FeatureServer does not (and need not) provide,
|
|
249
|
+
// so we skip-with-reason rather than fail. Other targets are unaffected.
|
|
250
|
+
if (targetMode === "standalone") {
|
|
251
|
+
return {
|
|
252
|
+
contract: "error-shape",
|
|
253
|
+
target: "(standalone)",
|
|
254
|
+
status: "skipped",
|
|
255
|
+
detail: "standalone surface uses MCP-native input validation (JSON-RPC -32602 InvalidParams); the GeoprocessingError envelope is a Honua-operator surface feature",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
216
258
|
const invalidInputs = buildInvalidArgsInputs();
|
|
217
259
|
const advertisedNames = new Set(advertised.map((t) => t.name));
|
|
218
260
|
const target = Object.keys(invalidInputs).find((name) => advertisedNames.has(name));
|
|
@@ -359,7 +401,7 @@ function collectKnownGaps(index, advertisedNames, referenceLookup) {
|
|
|
359
401
|
return gaps;
|
|
360
402
|
}
|
|
361
403
|
function assembleReport(args) {
|
|
362
|
-
const { serverInfo, index, options, tools, resources, prompts, contracts, knownGaps } = args;
|
|
404
|
+
const { serverInfo, index, options, tools, resources, prompts, contracts, knownGaps, provenance } = args;
|
|
363
405
|
const toolsSchemaValid = tools.filter((t) => t.schemaValid).length;
|
|
364
406
|
const conformanceChecked = tools.filter((t) => t.conformant !== null);
|
|
365
407
|
const toolsConformant = conformanceChecked.filter((t) => t.conformant === true).length;
|
|
@@ -368,6 +410,7 @@ function assembleReport(args) {
|
|
|
368
410
|
const contractsChecked = contracts.filter((c) => c.status !== "skipped").length;
|
|
369
411
|
const contractsPassed = contracts.filter((c) => c.status === "passed").length;
|
|
370
412
|
const contractsFailed = contracts.filter((c) => c.status === "failed").length;
|
|
413
|
+
const contractsSkipped = contracts.filter((c) => c.status === "skipped").length;
|
|
371
414
|
const toolFailures = tools.reduce((acc, t) => acc + t.errors.length, 0);
|
|
372
415
|
const failures = toolFailures + contractsFailed;
|
|
373
416
|
const pass = failures === 0;
|
|
@@ -383,6 +426,7 @@ function assembleReport(args) {
|
|
|
383
426
|
surface: options.surface,
|
|
384
427
|
},
|
|
385
428
|
standard: { source: STANDARD_SOURCE, indexDate: index.date, dialect: index.dialect },
|
|
429
|
+
provenance,
|
|
386
430
|
summary: {
|
|
387
431
|
pass,
|
|
388
432
|
toolsDiscovered: tools.length,
|
|
@@ -396,6 +440,7 @@ function assembleReport(args) {
|
|
|
396
440
|
contractsChecked,
|
|
397
441
|
contractsPassed,
|
|
398
442
|
contractsFailed,
|
|
443
|
+
contractsSkipped,
|
|
399
444
|
knownGaps: knownGaps.length,
|
|
400
445
|
failures,
|
|
401
446
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { runCertification, writeArtifacts } from "./run.js";
|
|
@@ -6,6 +7,7 @@ function parseArgs(argv) {
|
|
|
6
7
|
let artifactOnly = false;
|
|
7
8
|
// Default to the package root (two levels up from dist/src/certification/).
|
|
8
9
|
let outDir = fileURLToPath(new URL("../../../", import.meta.url));
|
|
10
|
+
let target;
|
|
9
11
|
for (let i = 0; i < argv.length; i++) {
|
|
10
12
|
const arg = argv[i];
|
|
11
13
|
if (arg === "--artifact-only") {
|
|
@@ -18,8 +20,15 @@ function parseArgs(argv) {
|
|
|
18
20
|
}
|
|
19
21
|
outDir = resolve(value);
|
|
20
22
|
}
|
|
23
|
+
else if (arg === "--target") {
|
|
24
|
+
const value = argv[++i];
|
|
25
|
+
if (!value) {
|
|
26
|
+
throw new Error("--target requires a target mode (offline | remote | stdio-proxy | standalone)");
|
|
27
|
+
}
|
|
28
|
+
target = value;
|
|
29
|
+
}
|
|
21
30
|
}
|
|
22
|
-
return { artifactOnly, outDir };
|
|
31
|
+
return { artifactOnly, outDir, ...(target ? { target } : {}) };
|
|
23
32
|
}
|
|
24
33
|
function artifactPaths(outDir) {
|
|
25
34
|
return {
|
|
@@ -28,15 +37,21 @@ function artifactPaths(outDir) {
|
|
|
28
37
|
};
|
|
29
38
|
}
|
|
30
39
|
async function main() {
|
|
31
|
-
const { artifactOnly, outDir } = parseArgs(process.argv.slice(2));
|
|
32
|
-
const
|
|
40
|
+
const { artifactOnly, outDir, target } = parseArgs(process.argv.slice(2));
|
|
41
|
+
const env = target ? { ...process.env, HONUA_MCP_CERT_TARGET: target } : process.env;
|
|
42
|
+
const report = await runCertification({ env });
|
|
43
|
+
mkdirSync(outDir, { recursive: true });
|
|
33
44
|
const paths = artifactPaths(outDir);
|
|
34
45
|
writeArtifacts(report, paths);
|
|
35
46
|
const s = report.summary;
|
|
36
47
|
const status = s.pass ? "PASS" : "FAIL";
|
|
37
48
|
process.stdout.write(`MCP certification ${status} [${report.protocol.targetMode}]: ${s.toolsConformant}/${s.toolsConformanceChecked} conformant, ` +
|
|
38
49
|
`${s.toolsOutputValidated} output-validated, ${s.contractsPassed}/${s.contractsChecked} contracts, ` +
|
|
39
|
-
`${s.knownGaps} known gaps, ${s.failures} failures.\n`);
|
|
50
|
+
`${s.contractsSkipped} skipped, ${s.knownGaps} known gaps, ${s.failures} failures.\n`);
|
|
51
|
+
const skipped = report.contracts.filter((c) => c.status === "skipped");
|
|
52
|
+
for (const c of skipped) {
|
|
53
|
+
process.stdout.write(` SKIP [${c.contract} / ${c.target}]: ${c.detail}\n`);
|
|
54
|
+
}
|
|
40
55
|
process.stdout.write(`Artifacts: ${paths.json}\n ${paths.markdown}\n`);
|
|
41
56
|
if (!artifactOnly && !report.summary.pass) {
|
|
42
57
|
process.exitCode = 1;
|
|
@@ -49,6 +49,47 @@ export declare function checkInvalidArgsContract(result: {
|
|
|
49
49
|
* envelope or as an AuthorizationDenied geoprocessing-error kind.
|
|
50
50
|
*/
|
|
51
51
|
export declare function checkAuthContract(source: unknown): ContractOutcome;
|
|
52
|
+
/** Documented job states, ordered by lifecycle rank (terminal states are highest). */
|
|
53
|
+
export declare const JOB_STATE_RANK: Record<string, number>;
|
|
54
|
+
/** Observations gathered from a mutating insert→verify→update→verify→delete→verify round-trip. */
|
|
55
|
+
export interface FeatureRoundTripObservations {
|
|
56
|
+
/** Object id assigned by the insert (undefined ⇒ insert did not return one). */
|
|
57
|
+
insertedObjectId: number | undefined;
|
|
58
|
+
/** The inserted feature was found by a follow-up query. */
|
|
59
|
+
presentAfterInsert: boolean;
|
|
60
|
+
/** Attribute value read back after the update. */
|
|
61
|
+
attributeAfterUpdate: unknown;
|
|
62
|
+
/** Attribute value the update wrote (what `attributeAfterUpdate` must equal). */
|
|
63
|
+
expectedAttribute: unknown;
|
|
64
|
+
/** The feature was absent from a follow-up query after the delete. */
|
|
65
|
+
absentAfterDelete: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* mutating-round-trip contract: an insert must be observable, an update must be
|
|
69
|
+
* reflected, and a delete must remove the feature — the full edit lifecycle
|
|
70
|
+
* verified against the surface's own read path.
|
|
71
|
+
*/
|
|
72
|
+
export declare function checkFeatureRoundTrip(obs: FeatureRoundTripObservations): ContractOutcome;
|
|
73
|
+
/** Observations gathered from an async job execute→poll→result lifecycle. */
|
|
74
|
+
export interface JobLifecycleObservations {
|
|
75
|
+
/** Status returned synchronously by execute (e.g. "Submitted"). */
|
|
76
|
+
initialStatus: string;
|
|
77
|
+
/** Statuses observed by polling the job resource, in poll order. */
|
|
78
|
+
polledStatuses: string[];
|
|
79
|
+
/** Progress fractions observed alongside the polled statuses (0..1). */
|
|
80
|
+
progressFractions: number[];
|
|
81
|
+
/** The lifecycle reached a documented terminal state. */
|
|
82
|
+
finalStatus: string;
|
|
83
|
+
/** A result package was fetchable and reported ready once the job succeeded. */
|
|
84
|
+
resultReady: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* async-job-lifecycle contract: an execute → poll(honua://jobs/{id}) → fetch-result
|
|
88
|
+
* flow must follow the documented state machine — statuses are all recognized,
|
|
89
|
+
* rank never regresses, progress is monotonic within [0,1], the job terminates
|
|
90
|
+
* (Succeeded here), and the result package is retrievable.
|
|
91
|
+
*/
|
|
92
|
+
export declare function checkJobLifecycle(obs: JobLifecycleObservations): ContractOutcome;
|
|
52
93
|
export interface ListPage {
|
|
53
94
|
/** Distinct item identity keys observed on this page (e.g. tool names). */
|
|
54
95
|
keys: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../../src/certification/contracts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,yEAA0E,CAAC;AAUxG,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,uBAAuB,GAAG,SAAS,CAgC3F;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wEAAwE;AACxE,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAG1E;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,GAAG,eAAe,CA8BlB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAoBlE;AAED,MAAM,WAAW,QAAQ;IACvB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,eAAe,CA6B1E"}
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../../src/certification/contracts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,yEAA0E,CAAC;AAUxG,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,uBAAuB,GAAG,SAAS,CAgC3F;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wEAAwE;AACxE,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAG1E;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,GAAG,eAAe,CA8BlB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAoBlE;AAED,sFAAsF;AACtF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOjD,CAAC;AAGF,kGAAkG;AAClG,MAAM,WAAW,4BAA4B;IAC3C,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,2DAA2D;IAC3D,kBAAkB,EAAE,OAAO,CAAC;IAC5B,kDAAkD;IAClD,oBAAoB,EAAE,OAAO,CAAC;IAC9B,iFAAiF;IACjF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,4BAA4B,GAAG,eAAe,CAmBxF;AAED,6EAA6E;AAC7E,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,wEAAwE;IACxE,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,wBAAwB,GAAG,eAAe,CAiChF;AAED,MAAM,WAAW,QAAQ;IACvB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,eAAe,CA6B1E"}
|
|
@@ -135,6 +135,77 @@ export function checkAuthContract(source) {
|
|
|
135
135
|
],
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
|
+
/** Documented job states, ordered by lifecycle rank (terminal states are highest). */
|
|
139
|
+
export const JOB_STATE_RANK = {
|
|
140
|
+
Submitted: 0,
|
|
141
|
+
Queued: 0,
|
|
142
|
+
Running: 1,
|
|
143
|
+
Succeeded: 2,
|
|
144
|
+
Failed: 2,
|
|
145
|
+
Cancelled: 2,
|
|
146
|
+
};
|
|
147
|
+
const TERMINAL_JOB_STATES = new Set(["Succeeded", "Failed", "Cancelled"]);
|
|
148
|
+
/**
|
|
149
|
+
* mutating-round-trip contract: an insert must be observable, an update must be
|
|
150
|
+
* reflected, and a delete must remove the feature — the full edit lifecycle
|
|
151
|
+
* verified against the surface's own read path.
|
|
152
|
+
*/
|
|
153
|
+
export function checkFeatureRoundTrip(obs) {
|
|
154
|
+
const errors = [];
|
|
155
|
+
if (obs.insertedObjectId === undefined) {
|
|
156
|
+
errors.push("insert did not return a new objectId");
|
|
157
|
+
}
|
|
158
|
+
if (!obs.presentAfterInsert) {
|
|
159
|
+
errors.push("inserted feature was not observable via a follow-up query");
|
|
160
|
+
}
|
|
161
|
+
if (obs.attributeAfterUpdate !== obs.expectedAttribute) {
|
|
162
|
+
errors.push(`updated attribute did not round-trip (expected ${JSON.stringify(obs.expectedAttribute)}, read ${JSON.stringify(obs.attributeAfterUpdate)})`);
|
|
163
|
+
}
|
|
164
|
+
if (!obs.absentAfterDelete) {
|
|
165
|
+
errors.push("feature was still observable after delete");
|
|
166
|
+
}
|
|
167
|
+
return { ok: errors.length === 0, errors };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* async-job-lifecycle contract: an execute → poll(honua://jobs/{id}) → fetch-result
|
|
171
|
+
* flow must follow the documented state machine — statuses are all recognized,
|
|
172
|
+
* rank never regresses, progress is monotonic within [0,1], the job terminates
|
|
173
|
+
* (Succeeded here), and the result package is retrievable.
|
|
174
|
+
*/
|
|
175
|
+
export function checkJobLifecycle(obs) {
|
|
176
|
+
const errors = [];
|
|
177
|
+
const allStatuses = [obs.initialStatus, ...obs.polledStatuses, obs.finalStatus];
|
|
178
|
+
let lastRank = -1;
|
|
179
|
+
for (const status of allStatuses) {
|
|
180
|
+
const rank = JOB_STATE_RANK[status];
|
|
181
|
+
if (rank === undefined) {
|
|
182
|
+
errors.push(`unrecognized job state "${status}" (not in the documented state machine)`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (rank < lastRank) {
|
|
186
|
+
errors.push(`job state regressed from rank ${lastRank} to "${status}" (rank ${rank})`);
|
|
187
|
+
}
|
|
188
|
+
lastRank = rank;
|
|
189
|
+
}
|
|
190
|
+
let lastFraction = -1;
|
|
191
|
+
for (const fraction of obs.progressFractions) {
|
|
192
|
+
if (typeof fraction !== "number" || Number.isNaN(fraction) || fraction < 0 || fraction > 1) {
|
|
193
|
+
errors.push(`progress fraction ${String(fraction)} is out of the documented [0,1] range`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (fraction < lastFraction) {
|
|
197
|
+
errors.push(`progress fraction regressed from ${lastFraction} to ${fraction}`);
|
|
198
|
+
}
|
|
199
|
+
lastFraction = fraction;
|
|
200
|
+
}
|
|
201
|
+
if (!TERMINAL_JOB_STATES.has(obs.finalStatus)) {
|
|
202
|
+
errors.push(`job did not reach a terminal state (ended "${obs.finalStatus}")`);
|
|
203
|
+
}
|
|
204
|
+
if (obs.finalStatus === "Succeeded" && !obs.resultReady) {
|
|
205
|
+
errors.push("job succeeded but no ready result package could be fetched");
|
|
206
|
+
}
|
|
207
|
+
return { ok: errors.length === 0, errors };
|
|
208
|
+
}
|
|
138
209
|
/**
|
|
139
210
|
* pagination contract: following `nextCursor` yields additional distinct items,
|
|
140
211
|
* never repeats a cursor (terminates), and the final page has no cursor.
|
|
@@ -22,6 +22,19 @@ export declare function resolveRoundTripEnv(env?: NodeJS.ProcessEnv): RoundTripE
|
|
|
22
22
|
* listed here.
|
|
23
23
|
*/
|
|
24
24
|
export declare function buildRoundTripInputs(env: RoundTripEnv): Record<string, Record<string, unknown>>;
|
|
25
|
+
/** Target layer for the mutating round-trip + pagination contracts. */
|
|
26
|
+
export interface EditTarget {
|
|
27
|
+
serviceId: string;
|
|
28
|
+
layerId: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the editable/pageable target for the deep contracts. Defaults to the
|
|
32
|
+
* offline mock's seeded `svc-parks` layer 0; override with
|
|
33
|
+
* `HONUA_MCP_EDIT_SERVICE_ID` / `HONUA_MCP_EDIT_LAYER_ID` (or the shared
|
|
34
|
+
* `HONUA_MCP_SERVICE_ID` / `HONUA_MCP_LAYER_ID`) to point a live mutating cert at
|
|
35
|
+
* a dedicated scratch service.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveEditTarget(env?: NodeJS.ProcessEnv): EditTarget;
|
|
25
38
|
/**
|
|
26
39
|
* A deliberately invalid argument set for the error-shape contract. Certification
|
|
27
40
|
* picks the first advertised tool that has an entry here and calls it with these
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../../src/certification/fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAQtF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../../src/certification/fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAQtF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsC/F;AAED,uEAAuE;AACvE,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,UAAU,CAOlF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMhF"}
|