@descryy/runtime-openapi-observation 0.0.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/dist/declared-roles.d.ts +27 -0
- package/dist/declared-roles.d.ts.map +1 -0
- package/dist/declared-roles.js +48 -0
- package/dist/declared-roles.js.map +1 -0
- package/dist/endpoint-comparison.d.ts +53 -0
- package/dist/endpoint-comparison.d.ts.map +1 -0
- package/dist/endpoint-comparison.js +40 -0
- package/dist/endpoint-comparison.js.map +1 -0
- package/dist/exchange-comparison.d.ts +57 -0
- package/dist/exchange-comparison.d.ts.map +1 -0
- package/dist/exchange-comparison.js +50 -0
- package/dist/exchange-comparison.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-comparison.d.ts +81 -0
- package/dist/schema-comparison.d.ts.map +1 -0
- package/dist/schema-comparison.js +121 -0
- package/dist/schema-comparison.js.map +1 -0
- package/dist/status-bucket.d.ts +21 -0
- package/dist/status-bucket.d.ts.map +1 -0
- package/dist/status-bucket.js +36 -0
- package/dist/status-bucket.js.map +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which DTO schemas a resolved `API_ENDPOINT` actually declares, by role —
|
|
3
|
+
* the two-hop join `adapter-openapi` (descry-adapters) writes but nothing
|
|
4
|
+
* here had read yet: `API_ROUTE --SERVES_API--> API_ENDPOINT`,
|
|
5
|
+
* `API_ROUTE --USES_TYPE(roles)--> DTO`. An endpoint resolves; its declared
|
|
6
|
+
* shape is one edge traversal further, through the route that serves it —
|
|
7
|
+
* `API_ENDPOINT` itself carries no `attrs.fields`, only `attrs.method`/
|
|
8
|
+
* `attrs.pathTemplate` (`adapter.ts`, read directly rather than assumed).
|
|
9
|
+
*
|
|
10
|
+
* **Multiple serving routes are folded, not refused.** Unlike
|
|
11
|
+
* `resolveEndpointNode`'s own "similar endpoint" ambiguity (which route
|
|
12
|
+
* served THIS request), several real routes legitimately serving one shared
|
|
13
|
+
* `API_ENDPOINT` (a repo-scoped route in each of several repositories,
|
|
14
|
+
* DEC-115) all declare the *same contract* by construction — the endpoint is
|
|
15
|
+
* the join point precisely because it is workspace-scoped and they are not.
|
|
16
|
+
* So every DTO any serving route declares, under whichever roles it declares
|
|
17
|
+
* them, is unioned rather than picked between.
|
|
18
|
+
*/
|
|
19
|
+
import type { IRNode } from "@descryy/ir";
|
|
20
|
+
import { type SqlDriver } from "@descryy/core";
|
|
21
|
+
export type SchemaRole = "request" | "response" | "errorResponse";
|
|
22
|
+
export type DeclaredSchemaRoles = {
|
|
23
|
+
readonly [K in SchemaRole]: readonly IRNode[];
|
|
24
|
+
};
|
|
25
|
+
/** `{request: [], response: [], errorResponse: []}` for an endpoint no route serves (should not happen for a `resolved` endpoint, but reported rather than assumed) or one whose serving route(s) declared no `USES_TYPE` edge at all — a real, disclosed "this operation names no schema", not an error. */
|
|
26
|
+
export declare function declaredSchemaRoles(driver: SqlDriver, endpointNodeId: string): DeclaredSchemaRoles;
|
|
27
|
+
//# sourceMappingURL=declared-roles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declared-roles.d.ts","sourceRoot":"","sources":["../src/declared-roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAE5E,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,eAAe,CAAC;AAElE,MAAM,MAAM,mBAAmB,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,UAAU,GAAG,SAAS,MAAM,EAAE;CAAE,CAAC;AAOpF,6SAA6S;AAC7S,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,GAAG,mBAAmB,CAqBlG"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which DTO schemas a resolved `API_ENDPOINT` actually declares, by role —
|
|
3
|
+
* the two-hop join `adapter-openapi` (descry-adapters) writes but nothing
|
|
4
|
+
* here had read yet: `API_ROUTE --SERVES_API--> API_ENDPOINT`,
|
|
5
|
+
* `API_ROUTE --USES_TYPE(roles)--> DTO`. An endpoint resolves; its declared
|
|
6
|
+
* shape is one edge traversal further, through the route that serves it —
|
|
7
|
+
* `API_ENDPOINT` itself carries no `attrs.fields`, only `attrs.method`/
|
|
8
|
+
* `attrs.pathTemplate` (`adapter.ts`, read directly rather than assumed).
|
|
9
|
+
*
|
|
10
|
+
* **Multiple serving routes are folded, not refused.** Unlike
|
|
11
|
+
* `resolveEndpointNode`'s own "similar endpoint" ambiguity (which route
|
|
12
|
+
* served THIS request), several real routes legitimately serving one shared
|
|
13
|
+
* `API_ENDPOINT` (a repo-scoped route in each of several repositories,
|
|
14
|
+
* DEC-115) all declare the *same contract* by construction — the endpoint is
|
|
15
|
+
* the join point precisely because it is workspace-scoped and they are not.
|
|
16
|
+
* So every DTO any serving route declares, under whichever roles it declares
|
|
17
|
+
* them, is unioned rather than picked between.
|
|
18
|
+
*/
|
|
19
|
+
import { getNode, incoming, outgoing } from "@descryy/core";
|
|
20
|
+
function rolesOf(attrs) {
|
|
21
|
+
const roles = attrs?.roles;
|
|
22
|
+
return Array.isArray(roles) ? roles.filter((r) => typeof r === "string") : [];
|
|
23
|
+
}
|
|
24
|
+
/** `{request: [], response: [], errorResponse: []}` for an endpoint no route serves (should not happen for a `resolved` endpoint, but reported rather than assumed) or one whose serving route(s) declared no `USES_TYPE` edge at all — a real, disclosed "this operation names no schema", not an error. */
|
|
25
|
+
export function declaredSchemaRoles(driver, endpointNodeId) {
|
|
26
|
+
const result = { request: [], response: [], errorResponse: [] };
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const servingRoutes = incoming(driver, endpointNodeId, { types: ["SERVES_API"] });
|
|
29
|
+
for (const routeEdge of servingRoutes) {
|
|
30
|
+
const usesType = outgoing(driver, routeEdge.from, { types: ["USES_TYPE"] });
|
|
31
|
+
for (const edge of usesType) {
|
|
32
|
+
const dto = getNode(driver, edge.to);
|
|
33
|
+
if (dto === undefined)
|
|
34
|
+
continue; // a dangling edge target: a disclosed gap, not a schema to invent
|
|
35
|
+
for (const role of rolesOf(edge.attrs)) {
|
|
36
|
+
if (role !== "request" && role !== "response" && role !== "errorResponse")
|
|
37
|
+
continue;
|
|
38
|
+
const key = `${role}:${dto.id}`;
|
|
39
|
+
if (seen.has(key))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(key);
|
|
42
|
+
result[role].push(dto);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=declared-roles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declared-roles.js","sourceRoot":"","sources":["../src/declared-roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAkB,MAAM,eAAe,CAAC;AAM5E,SAAS,OAAO,CAAC,KAAoD;IACnE,MAAM,KAAK,GAAG,KAAK,EAAE,KAAK,CAAC;IAC3B,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7F,CAAC;AAED,6SAA6S;AAC7S,MAAM,UAAU,mBAAmB,CAAC,MAAiB,EAAE,cAAsB;IAC3E,MAAM,MAAM,GAAoC,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC;IACjG,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IAClF,KAAK,MAAM,SAAS,IAAI,aAAa,EAAE,CAAC;QACtC,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QAC5E,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;YACrC,IAAI,GAAG,KAAK,SAAS;gBAAE,SAAS,CAAC,kEAAkE;YACnG,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,eAAe;oBAAE,SAAS;gBACpF,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;gBAChC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,SAAS;gBAC5B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACd,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving an observed request's method+path against the graph's declared
|
|
3
|
+
* `API_ENDPOINT` shapes — checklist §15's "documented endpoint" /
|
|
4
|
+
* "undocumented endpoint" / "wrong method" boxes, and §18's "table/column
|
|
5
|
+
* mapping" analogue for the OpenAPI half: does the contract know about this
|
|
6
|
+
* call at all, and if not, is that because the path was never declared, or
|
|
7
|
+
* because it was declared under a different verb.
|
|
8
|
+
*
|
|
9
|
+
* Both underlying queries (`resolveEndpointNode`, `resolveEndpointNodesByPath`)
|
|
10
|
+
* are `@descryy/runtime-graph-correlator`'s, reused rather than duplicated —
|
|
11
|
+
* `resolveEndpointNodesByPath` exists in that package specifically because
|
|
12
|
+
* this comparison needed it (a method-agnostic path query resolveEndpointNode
|
|
13
|
+
* deliberately doesn't answer on its own).
|
|
14
|
+
*/
|
|
15
|
+
import type { IRNode } from "@descryy/ir";
|
|
16
|
+
import type { SqlDriver } from "@descryy/core";
|
|
17
|
+
export type EndpointComparison =
|
|
18
|
+
/** Exact method+path resolved to exactly one declared operation. */
|
|
19
|
+
{
|
|
20
|
+
readonly outcome: "documented";
|
|
21
|
+
readonly endpoint: IRNode;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Neither the exact method+path, nor the path under any other method,
|
|
25
|
+
* resolves to anything — the operation was never declared, under any verb.
|
|
26
|
+
*/
|
|
27
|
+
| {
|
|
28
|
+
readonly outcome: "undocumented";
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The path IS declared — just not for the method that was actually used.
|
|
33
|
+
* A different fact from `undocumented`: this is what a contract-reading
|
|
34
|
+
* person would call a client bug, not a documentation gap.
|
|
35
|
+
*/
|
|
36
|
+
| {
|
|
37
|
+
readonly outcome: "wrong-method";
|
|
38
|
+
readonly observedMethod: string;
|
|
39
|
+
readonly declaredUnderMethods: readonly string[];
|
|
40
|
+
readonly reason: string;
|
|
41
|
+
}
|
|
42
|
+
/** Passed straight through from the underlying resolver — same "similar endpoint" shape, one layer up. */
|
|
43
|
+
| {
|
|
44
|
+
readonly outcome: "ambiguous";
|
|
45
|
+
readonly candidates: readonly IRNode[];
|
|
46
|
+
readonly reason: string;
|
|
47
|
+
};
|
|
48
|
+
export interface ObservedRequest {
|
|
49
|
+
readonly method: string;
|
|
50
|
+
readonly path: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function compareObservedEndpoint(driver: SqlDriver, observed: ObservedRequest): EndpointComparison;
|
|
53
|
+
//# sourceMappingURL=endpoint-comparison.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoint-comparison.d.ts","sourceRoot":"","sources":["../src/endpoint-comparison.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG/C,MAAM,MAAM,kBAAkB;AAC5B,oEAAoE;AAClE;IAAE,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AAC/D;;;GAGG;GACD;IAAE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC/D;;;;GAIG;GACD;IACE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AACH,0GAA0G;GACxG;IAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,GAAG,kBAAkB,CA2BxG"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving an observed request's method+path against the graph's declared
|
|
3
|
+
* `API_ENDPOINT` shapes — checklist §15's "documented endpoint" /
|
|
4
|
+
* "undocumented endpoint" / "wrong method" boxes, and §18's "table/column
|
|
5
|
+
* mapping" analogue for the OpenAPI half: does the contract know about this
|
|
6
|
+
* call at all, and if not, is that because the path was never declared, or
|
|
7
|
+
* because it was declared under a different verb.
|
|
8
|
+
*
|
|
9
|
+
* Both underlying queries (`resolveEndpointNode`, `resolveEndpointNodesByPath`)
|
|
10
|
+
* are `@descryy/runtime-graph-correlator`'s, reused rather than duplicated —
|
|
11
|
+
* `resolveEndpointNodesByPath` exists in that package specifically because
|
|
12
|
+
* this comparison needed it (a method-agnostic path query resolveEndpointNode
|
|
13
|
+
* deliberately doesn't answer on its own).
|
|
14
|
+
*/
|
|
15
|
+
import { resolveEndpointNode, resolveEndpointNodesByPath } from "@descryy/runtime-graph-correlator";
|
|
16
|
+
export function compareObservedEndpoint(driver, observed) {
|
|
17
|
+
const exact = resolveEndpointNode(driver, observed.method, observed.path);
|
|
18
|
+
if (exact.outcome === "resolved") {
|
|
19
|
+
return { outcome: "documented", endpoint: exact.node };
|
|
20
|
+
}
|
|
21
|
+
if (exact.outcome === "ambiguous") {
|
|
22
|
+
return { outcome: "ambiguous", candidates: exact.candidates, reason: exact.reason };
|
|
23
|
+
}
|
|
24
|
+
// exact.outcome === "not-found" -- the exact method+path is not declared.
|
|
25
|
+
// Whether the *path* is declared under some other method is a separate
|
|
26
|
+
// question resolveEndpointNode's own method filter cannot answer.
|
|
27
|
+
const byPath = resolveEndpointNodesByPath(driver, observed.path);
|
|
28
|
+
if (byPath.nodes.length === 0) {
|
|
29
|
+
return { outcome: "undocumented", reason: exact.reason };
|
|
30
|
+
}
|
|
31
|
+
const declaredUnderMethods = [...new Set(byPath.nodes.map((n) => n.name.slice(0, n.name.indexOf(" "))))].sort();
|
|
32
|
+
return {
|
|
33
|
+
outcome: "wrong-method",
|
|
34
|
+
observedMethod: observed.method.toUpperCase(),
|
|
35
|
+
declaredUnderMethods,
|
|
36
|
+
reason: `${observed.path} is declared under ${declaredUnderMethods.join(", ")}, not ` +
|
|
37
|
+
`${observed.method.toUpperCase()} — the path is documented, the verb used was not.`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=endpoint-comparison.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoint-comparison.js","sourceRoot":"","sources":["../src/endpoint-comparison.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,mCAAmC,CAAC;AA6BpG,MAAM,UAAU,uBAAuB,CAAC,MAAiB,EAAE,QAAyB;IAClF,MAAM,KAAK,GAAG,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IAE1E,IAAI,KAAK,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;IACzD,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IACtF,CAAC;IAED,0EAA0E;IAC1E,uEAAuE;IACvE,kEAAkE;IAClE,MAAM,MAAM,GAAG,0BAA0B,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IAC3D,CAAC;IAED,MAAM,oBAAoB,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAChH,OAAO;QACL,OAAO,EAAE,cAAc;QACvB,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE;QAC7C,oBAAoB;QACpB,MAAM,EACJ,GAAG,QAAQ,CAAC,IAAI,sBAAsB,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ;YAC7E,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,mDAAmD;KACtF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ties `compareObservedEndpoint`, `classifyStatus`, `declaredSchemaRoles` and
|
|
3
|
+
* `compareBodyAgainstSchema` together into the one comparison checklist
|
|
4
|
+
* §15's own boxes actually describe: is the call documented, did its
|
|
5
|
+
* outcome's class match a schema role the contract declares for it, and —
|
|
6
|
+
* when a body was observed — does that body's own fields match that role's
|
|
7
|
+
* declared shape.
|
|
8
|
+
*
|
|
9
|
+
* **What "surprising" claims, precisely, and what it does not.** The
|
|
10
|
+
* contract (`adapter-openapi`) carries `responseSchemas`/`errorSchemas` by
|
|
11
|
+
* *role*, never a specific status code (§14/§18's own DECISIONS entry names
|
|
12
|
+
* this limit). So this can say "the operation's contract declares zero
|
|
13
|
+
* schemas for outcomes shaped like this one" — a real, checkable fact — and
|
|
14
|
+
* cannot say "this contract expected 200, not 201." Reported as
|
|
15
|
+
* `statusSurprising: boolean | null`, never as a confident status-code
|
|
16
|
+
* mismatch it has no basis for.
|
|
17
|
+
*
|
|
18
|
+
* **Request and response are compared against different roles, on purpose.**
|
|
19
|
+
* A request body is always checked against the `request` role, regardless of
|
|
20
|
+
* outcome; a response body is checked against whichever role the *observed*
|
|
21
|
+
* status bucket expects (`response` for success, `errorResponse` for a client
|
|
22
|
+
* or server error) — the same operation's success and error bodies are
|
|
23
|
+
* different contracts, and comparing an error body against the success DTO
|
|
24
|
+
* would report every field of a real, correct error response as missing.
|
|
25
|
+
*/
|
|
26
|
+
import type { SqlDriver } from "@descryy/core";
|
|
27
|
+
import { type EndpointComparison, type ObservedRequest } from "./endpoint-comparison.ts";
|
|
28
|
+
import { type DeclaredSchemaRoles } from "./declared-roles.ts";
|
|
29
|
+
import { type StatusBucket } from "./status-bucket.ts";
|
|
30
|
+
import { type SchemaComparison } from "./schema-comparison.ts";
|
|
31
|
+
export interface ObservedExchange extends ObservedRequest {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
/** The same raw `string | null` `browser-network-collector` already captures. Omitted (`undefined`) means "not observed", same as `null`. */
|
|
34
|
+
readonly requestBody?: string | null;
|
|
35
|
+
readonly responseBody?: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface ExchangeComparison {
|
|
38
|
+
readonly endpoint: EndpointComparison;
|
|
39
|
+
readonly statusBucket: StatusBucket;
|
|
40
|
+
/** `null` only when `endpoint.outcome !== "documented"` -- nothing to compare a status against without a resolved contract. */
|
|
41
|
+
readonly declaredRoles: DeclaredSchemaRoles | null;
|
|
42
|
+
/**
|
|
43
|
+
* `true`: the observed outcome's bucket has an expected role
|
|
44
|
+
* (`success`->`response`, `client-error`/`server-error`->`errorResponse`)
|
|
45
|
+
* and the endpoint's contract declares zero schemas under it.
|
|
46
|
+
* `false`: a schema is declared, or the endpoint isn't resolved cleanly
|
|
47
|
+
* enough to ask. `null`: the bucket (`informational`/`redirect`) has no
|
|
48
|
+
* role in this adapter's vocabulary to compare against at all.
|
|
49
|
+
*/
|
|
50
|
+
readonly statusSurprising: boolean | null;
|
|
51
|
+
/** `null` only when `endpoint.outcome !== "documented"`. Always compared against the `request` role, independent of the observed status. */
|
|
52
|
+
readonly requestSchema: SchemaComparison | null;
|
|
53
|
+
/** `null` when the endpoint isn't resolved, or the status bucket has no expected role (`informational`/`redirect`) to compare against. */
|
|
54
|
+
readonly responseSchema: SchemaComparison | null;
|
|
55
|
+
}
|
|
56
|
+
export declare function compareObservedExchange(driver: SqlDriver, observed: ObservedExchange): ExchangeComparison;
|
|
57
|
+
//# sourceMappingURL=exchange-comparison.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exchange-comparison.d.ts","sourceRoot":"","sources":["../src/exchange-comparison.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAA2B,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAClH,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6IAA6I;IAC7I,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,+HAA+H;IAC/H,QAAQ,CAAC,aAAa,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACnD;;;;;;;OAOG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,GAAG,IAAI,CAAC;IAC1C,4IAA4I;IAC5I,QAAQ,CAAC,aAAa,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAChD,0IAA0I;IAC1I,QAAQ,CAAC,cAAc,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAClD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,CAwBzG"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ties `compareObservedEndpoint`, `classifyStatus`, `declaredSchemaRoles` and
|
|
3
|
+
* `compareBodyAgainstSchema` together into the one comparison checklist
|
|
4
|
+
* §15's own boxes actually describe: is the call documented, did its
|
|
5
|
+
* outcome's class match a schema role the contract declares for it, and —
|
|
6
|
+
* when a body was observed — does that body's own fields match that role's
|
|
7
|
+
* declared shape.
|
|
8
|
+
*
|
|
9
|
+
* **What "surprising" claims, precisely, and what it does not.** The
|
|
10
|
+
* contract (`adapter-openapi`) carries `responseSchemas`/`errorSchemas` by
|
|
11
|
+
* *role*, never a specific status code (§14/§18's own DECISIONS entry names
|
|
12
|
+
* this limit). So this can say "the operation's contract declares zero
|
|
13
|
+
* schemas for outcomes shaped like this one" — a real, checkable fact — and
|
|
14
|
+
* cannot say "this contract expected 200, not 201." Reported as
|
|
15
|
+
* `statusSurprising: boolean | null`, never as a confident status-code
|
|
16
|
+
* mismatch it has no basis for.
|
|
17
|
+
*
|
|
18
|
+
* **Request and response are compared against different roles, on purpose.**
|
|
19
|
+
* A request body is always checked against the `request` role, regardless of
|
|
20
|
+
* outcome; a response body is checked against whichever role the *observed*
|
|
21
|
+
* status bucket expects (`response` for success, `errorResponse` for a client
|
|
22
|
+
* or server error) — the same operation's success and error bodies are
|
|
23
|
+
* different contracts, and comparing an error body against the success DTO
|
|
24
|
+
* would report every field of a real, correct error response as missing.
|
|
25
|
+
*/
|
|
26
|
+
import { compareObservedEndpoint } from "./endpoint-comparison.js";
|
|
27
|
+
import { declaredSchemaRoles } from "./declared-roles.js";
|
|
28
|
+
import { classifyStatus, expectedRoleForBucket } from "./status-bucket.js";
|
|
29
|
+
import { compareBodyAgainstSchema } from "./schema-comparison.js";
|
|
30
|
+
export function compareObservedExchange(driver, observed) {
|
|
31
|
+
const endpoint = compareObservedEndpoint(driver, observed);
|
|
32
|
+
const statusBucket = classifyStatus(observed.status);
|
|
33
|
+
if (endpoint.outcome !== "documented") {
|
|
34
|
+
return {
|
|
35
|
+
endpoint,
|
|
36
|
+
statusBucket,
|
|
37
|
+
declaredRoles: null,
|
|
38
|
+
statusSurprising: null,
|
|
39
|
+
requestSchema: null,
|
|
40
|
+
responseSchema: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const declaredRoles = declaredSchemaRoles(driver, endpoint.endpoint.id);
|
|
44
|
+
const expectedRole = expectedRoleForBucket(statusBucket);
|
|
45
|
+
const statusSurprising = expectedRole === null ? null : declaredRoles[expectedRole].length === 0;
|
|
46
|
+
const requestSchema = compareBodyAgainstSchema(declaredRoles.request, observed.requestBody);
|
|
47
|
+
const responseSchema = expectedRole === null ? null : compareBodyAgainstSchema(declaredRoles[expectedRole], observed.responseBody);
|
|
48
|
+
return { endpoint, statusBucket, declaredRoles, statusSurprising, requestSchema, responseSchema };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=exchange-comparison.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exchange-comparison.js","sourceRoot":"","sources":["../src/exchange-comparison.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAAE,uBAAuB,EAAiD,MAAM,0BAA0B,CAAC;AAClH,OAAO,EAAE,mBAAmB,EAA4B,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAqB,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAAE,wBAAwB,EAAyB,MAAM,wBAAwB,CAAC;AA6BzF,MAAM,UAAU,uBAAuB,CAAC,MAAiB,EAAE,QAA0B;IACnF,MAAM,QAAQ,GAAG,uBAAuB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC3D,MAAM,YAAY,GAAG,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAErD,IAAI,QAAQ,CAAC,OAAO,KAAK,YAAY,EAAE,CAAC;QACtC,OAAO;YACL,QAAQ;YACR,YAAY;YACZ,aAAa,EAAE,IAAI;YACnB,gBAAgB,EAAE,IAAI;YACtB,aAAa,EAAE,IAAI;YACnB,cAAc,EAAE,IAAI;SACrB,CAAC;IACJ,CAAC;IAED,MAAM,aAAa,GAAG,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACxE,MAAM,YAAY,GAAG,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACzD,MAAM,gBAAgB,GAAG,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;IAEjG,MAAM,aAAa,GAAG,wBAAwB,CAAC,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC5F,MAAM,cAAc,GAClB,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,wBAAwB,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;IAE9G,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;AACpG,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { EndpointComparison, ObservedRequest } from "./endpoint-comparison.ts";
|
|
2
|
+
export { compareObservedEndpoint } from "./endpoint-comparison.ts";
|
|
3
|
+
export type { SchemaRole, DeclaredSchemaRoles } from "./declared-roles.ts";
|
|
4
|
+
export { declaredSchemaRoles } from "./declared-roles.ts";
|
|
5
|
+
export { STATUS_BUCKETS, type StatusBucket, classifyStatus, expectedRoleForBucket } from "./status-bucket.ts";
|
|
6
|
+
export type { FieldDetail, FieldMismatch, SchemaComparison } from "./schema-comparison.ts";
|
|
7
|
+
export { compareBodyAgainstSchema } from "./schema-comparison.ts";
|
|
8
|
+
export type { ObservedExchange, ExchangeComparison } from "./exchange-comparison.ts";
|
|
9
|
+
export { compareObservedExchange } from "./exchange-comparison.ts";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACpF,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,YAAY,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE9G,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC3F,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAElE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { compareObservedEndpoint } from "./endpoint-comparison.js";
|
|
2
|
+
export { declaredSchemaRoles } from "./declared-roles.js";
|
|
3
|
+
export { STATUS_BUCKETS, classifyStatus, expectedRoleForBucket } from "./status-bucket.js";
|
|
4
|
+
export { compareBodyAgainstSchema } from "./schema-comparison.js";
|
|
5
|
+
export { compareObservedExchange } from "./exchange-comparison.js";
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAGnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAqB,cAAc,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAG9G,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAGlE,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row §15 left open: an observed request/response BODY compared against a
|
|
3
|
+
* declared DTO's fields, not merely *which* DTOs a role names
|
|
4
|
+
* (`declaredSchemaRoles`, which stops at "here are the schemas"). Reads
|
|
5
|
+
* `DTO.attrs.fieldDetail` — `{name, nullable, required, type?}[]`, written by
|
|
6
|
+
* `adapter-openapi`'s own `document.ts` directly from the contract, never
|
|
7
|
+
* derived here.
|
|
8
|
+
*
|
|
9
|
+
* **Comparable to the resolved graph, not to a stream.** This takes a raw
|
|
10
|
+
* body already in hand (the same `string | null` `browser-network-collector`
|
|
11
|
+
* already captures) — parsing and shape-checking are this file's job, sourcing
|
|
12
|
+
* the bytes is not, matching every other comparator in this package.
|
|
13
|
+
*
|
|
14
|
+
* **Extra fields are never reported.** This adapter does not capture
|
|
15
|
+
* `additionalProperties`, so "the body has a field the contract doesn't
|
|
16
|
+
* mention" is not a claim the contract supports either way — reporting it
|
|
17
|
+
* would be inventing a stricter contract than the one that was published.
|
|
18
|
+
*
|
|
19
|
+
* **A role naming more than one DTO refuses, rather than picks one.**
|
|
20
|
+
* `declaredSchemaRoles` unions across every serving route by design (DEC-115,
|
|
21
|
+
* multiple repo-scoped routes serving one workspace endpoint) — ordinarily
|
|
22
|
+
* they agree, but when they name two different schemas for the same role,
|
|
23
|
+
* comparing the body against whichever came first would silently prefer one
|
|
24
|
+
* repository's contract over another's.
|
|
25
|
+
*/
|
|
26
|
+
import type { IRNode } from "@descryy/ir";
|
|
27
|
+
export interface FieldDetail {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly nullable: boolean;
|
|
30
|
+
readonly required: boolean;
|
|
31
|
+
readonly type?: string;
|
|
32
|
+
}
|
|
33
|
+
export type FieldMismatch = {
|
|
34
|
+
readonly kind: "missing-required";
|
|
35
|
+
readonly field: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: "type-mismatch";
|
|
38
|
+
readonly field: string;
|
|
39
|
+
readonly declaredType: string;
|
|
40
|
+
readonly observedType: string;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: "null-not-nullable";
|
|
43
|
+
readonly field: string;
|
|
44
|
+
};
|
|
45
|
+
export type SchemaComparison =
|
|
46
|
+
/** No DTO is declared for this role at all — nothing to compare against. */
|
|
47
|
+
{
|
|
48
|
+
readonly outcome: "no-schema-declared";
|
|
49
|
+
}
|
|
50
|
+
/** More than one serving route declares a different DTO for the same role. */
|
|
51
|
+
| {
|
|
52
|
+
readonly outcome: "ambiguous-schema";
|
|
53
|
+
readonly candidates: readonly IRNode[];
|
|
54
|
+
readonly reason: string;
|
|
55
|
+
}
|
|
56
|
+
/** A DTO is declared, but no body was observed to compare it against (e.g. a GET, or a 204). */
|
|
57
|
+
| {
|
|
58
|
+
readonly outcome: "no-body-observed";
|
|
59
|
+
readonly schema: IRNode;
|
|
60
|
+
}
|
|
61
|
+
/** A body was observed but is not valid JSON — a DTO's fields cannot be read off it. */
|
|
62
|
+
| {
|
|
63
|
+
readonly outcome: "unparsable-body";
|
|
64
|
+
readonly schema: IRNode;
|
|
65
|
+
readonly reason: string;
|
|
66
|
+
}
|
|
67
|
+
/** Valid JSON, but not an object — a DTO's fields have nothing to land on. */
|
|
68
|
+
| {
|
|
69
|
+
readonly outcome: "not-an-object";
|
|
70
|
+
readonly schema: IRNode;
|
|
71
|
+
readonly observedType: string;
|
|
72
|
+
} | {
|
|
73
|
+
readonly outcome: "match";
|
|
74
|
+
readonly schema: IRNode;
|
|
75
|
+
} | {
|
|
76
|
+
readonly outcome: "mismatch";
|
|
77
|
+
readonly schema: IRNode;
|
|
78
|
+
readonly mismatches: readonly FieldMismatch[];
|
|
79
|
+
};
|
|
80
|
+
export declare function compareBodyAgainstSchema(schemas: readonly IRNode[], rawBody: string | null | undefined): SchemaComparison;
|
|
81
|
+
//# sourceMappingURL=schema-comparison.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-comparison.d.ts","sourceRoot":"","sources":["../src/schema-comparison.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACxH;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE,MAAM,MAAM,gBAAgB;AAC1B,4EAA4E;AAC1E;IAAE,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE;AAC5C,8EAA8E;GAC5E;IAAE,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC3G,gGAAgG;GAC9F;IAAE,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AACnE,wFAAwF;GACtF;IAAE,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE;AAC3F,8EAA8E;GAC5E;IAAE,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC7F;IAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAA;CAAE,CAAC;AAwC7G,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,gBAAgB,CA+DzH"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The row §15 left open: an observed request/response BODY compared against a
|
|
3
|
+
* declared DTO's fields, not merely *which* DTOs a role names
|
|
4
|
+
* (`declaredSchemaRoles`, which stops at "here are the schemas"). Reads
|
|
5
|
+
* `DTO.attrs.fieldDetail` — `{name, nullable, required, type?}[]`, written by
|
|
6
|
+
* `adapter-openapi`'s own `document.ts` directly from the contract, never
|
|
7
|
+
* derived here.
|
|
8
|
+
*
|
|
9
|
+
* **Comparable to the resolved graph, not to a stream.** This takes a raw
|
|
10
|
+
* body already in hand (the same `string | null` `browser-network-collector`
|
|
11
|
+
* already captures) — parsing and shape-checking are this file's job, sourcing
|
|
12
|
+
* the bytes is not, matching every other comparator in this package.
|
|
13
|
+
*
|
|
14
|
+
* **Extra fields are never reported.** This adapter does not capture
|
|
15
|
+
* `additionalProperties`, so "the body has a field the contract doesn't
|
|
16
|
+
* mention" is not a claim the contract supports either way — reporting it
|
|
17
|
+
* would be inventing a stricter contract than the one that was published.
|
|
18
|
+
*
|
|
19
|
+
* **A role naming more than one DTO refuses, rather than picks one.**
|
|
20
|
+
* `declaredSchemaRoles` unions across every serving route by design (DEC-115,
|
|
21
|
+
* multiple repo-scoped routes serving one workspace endpoint) — ordinarily
|
|
22
|
+
* they agree, but when they name two different schemas for the same role,
|
|
23
|
+
* comparing the body against whichever came first would silently prefer one
|
|
24
|
+
* repository's contract over another's.
|
|
25
|
+
*/
|
|
26
|
+
function isFieldDetail(value) {
|
|
27
|
+
if (typeof value !== "object" || value === null)
|
|
28
|
+
return false;
|
|
29
|
+
const v = value;
|
|
30
|
+
return (typeof v.name === "string" &&
|
|
31
|
+
typeof v.nullable === "boolean" &&
|
|
32
|
+
typeof v.required === "boolean" &&
|
|
33
|
+
(v.type === undefined || typeof v.type === "string"));
|
|
34
|
+
}
|
|
35
|
+
/** Malformed entries are dropped, not thrown on — the same defensive reading `rolesOf` (declared-roles.ts) does for edge attrs. */
|
|
36
|
+
function fieldDetailOf(schema) {
|
|
37
|
+
const raw = schema.attrs?.fieldDetail;
|
|
38
|
+
return Array.isArray(raw) ? raw.filter(isFieldDetail) : [];
|
|
39
|
+
}
|
|
40
|
+
function observedTypeOf(value) {
|
|
41
|
+
if (value === null)
|
|
42
|
+
return "null";
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
return "array";
|
|
45
|
+
return typeof value;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* JSON's own type system has no `integer` — `5` and `5.5` are both
|
|
49
|
+
* `"number"`. An `integer`-declared field is checked against
|
|
50
|
+
* `Number.isInteger`, which is the only place declared and observed types
|
|
51
|
+
* are not a plain `typeof` match.
|
|
52
|
+
*/
|
|
53
|
+
const TYPE_CHECKS = {
|
|
54
|
+
string: (v) => typeof v === "string",
|
|
55
|
+
number: (v) => typeof v === "number",
|
|
56
|
+
integer: (v) => typeof v === "number" && Number.isInteger(v),
|
|
57
|
+
boolean: (v) => typeof v === "boolean",
|
|
58
|
+
array: (v) => Array.isArray(v),
|
|
59
|
+
object: (v) => typeof v === "object" && v !== null && !Array.isArray(v),
|
|
60
|
+
};
|
|
61
|
+
export function compareBodyAgainstSchema(schemas, rawBody) {
|
|
62
|
+
if (schemas.length === 0)
|
|
63
|
+
return { outcome: "no-schema-declared" };
|
|
64
|
+
if (schemas.length > 1) {
|
|
65
|
+
return {
|
|
66
|
+
outcome: "ambiguous-schema",
|
|
67
|
+
candidates: schemas,
|
|
68
|
+
reason: `${schemas.length} serving routes declare different schemas for this role ` +
|
|
69
|
+
`(${schemas.map((s) => s.name).join(", ")}) — picking one to compare the body against would be a guess.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const schema = schemas[0]; // length checked above: not 0, not >1.
|
|
73
|
+
if (rawBody === null || rawBody === undefined) {
|
|
74
|
+
return { outcome: "no-body-observed", schema };
|
|
75
|
+
}
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(rawBody);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return {
|
|
82
|
+
outcome: "unparsable-body",
|
|
83
|
+
schema,
|
|
84
|
+
reason: "the observed body is not valid JSON, so field-level comparison against a JSON schema is not possible",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
88
|
+
return { outcome: "not-an-object", schema, observedType: observedTypeOf(parsed) };
|
|
89
|
+
}
|
|
90
|
+
const body = parsed;
|
|
91
|
+
const mismatches = [];
|
|
92
|
+
for (const field of fieldDetailOf(schema)) {
|
|
93
|
+
if (!Object.prototype.hasOwnProperty.call(body, field.name)) {
|
|
94
|
+
if (field.required)
|
|
95
|
+
mismatches.push({ kind: "missing-required", field: field.name });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const value = body[field.name];
|
|
99
|
+
if (value === null) {
|
|
100
|
+
if (!field.nullable)
|
|
101
|
+
mismatches.push({ kind: "null-not-nullable", field: field.name });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (field.type === undefined)
|
|
105
|
+
continue;
|
|
106
|
+
const check = TYPE_CHECKS[field.type];
|
|
107
|
+
// An unrecognised declared type (the document names something outside
|
|
108
|
+
// this map) is not checked, rather than silently treated as a match —
|
|
109
|
+
// there is nothing here yet to compare it against.
|
|
110
|
+
if (check !== undefined && !check(value)) {
|
|
111
|
+
mismatches.push({
|
|
112
|
+
kind: "type-mismatch",
|
|
113
|
+
field: field.name,
|
|
114
|
+
declaredType: field.type,
|
|
115
|
+
observedType: observedTypeOf(value),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return mismatches.length === 0 ? { outcome: "match", schema } : { outcome: "mismatch", schema, mismatches };
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=schema-comparison.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-comparison.js","sourceRoot":"","sources":["../src/schema-comparison.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A real HTTP status classified into the class an OpenAPI document actually
|
|
3
|
+
* has a role vocabulary for. `adapter-openapi`'s `Operation` (descry-adapters)
|
|
4
|
+
* carries `responseSchemas` (2xx) and `errorSchemas` (non-2xx) — two roles,
|
|
5
|
+
* not eleven status codes — so this reader stops at the same resolution the
|
|
6
|
+
* contract itself does, rather than inventing a numeric comparison the
|
|
7
|
+
* document was never asked to support.
|
|
8
|
+
*/
|
|
9
|
+
export declare const STATUS_BUCKETS: readonly ["informational", "success", "redirect", "client-error", "server-error"];
|
|
10
|
+
export type StatusBucket = (typeof STATUS_BUCKETS)[number];
|
|
11
|
+
export declare function classifyStatus(status: number): StatusBucket;
|
|
12
|
+
/**
|
|
13
|
+
* The DTO role (`adapter.ts`'s own `roles` vocabulary: `"request"` /
|
|
14
|
+
* `"response"` / `"errorResponse"`) a bucket corresponds to — `null` for
|
|
15
|
+
* informational/redirect, which `adapter-openapi` never assigns a role to at
|
|
16
|
+
* all. Refused rather than guessed: reporting "no declared schema" for a
|
|
17
|
+
* bucket the contract was never asked about would read as a finding where
|
|
18
|
+
* there is only an unmodelled case.
|
|
19
|
+
*/
|
|
20
|
+
export declare function expectedRoleForBucket(bucket: StatusBucket): "response" | "errorResponse" | null;
|
|
21
|
+
//# sourceMappingURL=status-bucket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-bucket.d.ts","sourceRoot":"","sources":["../src/status-bucket.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,eAAO,MAAM,cAAc,mFAAoF,CAAC;AAChH,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAM3D;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,UAAU,GAAG,eAAe,GAAG,IAAI,CAI/F"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A real HTTP status classified into the class an OpenAPI document actually
|
|
3
|
+
* has a role vocabulary for. `adapter-openapi`'s `Operation` (descry-adapters)
|
|
4
|
+
* carries `responseSchemas` (2xx) and `errorSchemas` (non-2xx) — two roles,
|
|
5
|
+
* not eleven status codes — so this reader stops at the same resolution the
|
|
6
|
+
* contract itself does, rather than inventing a numeric comparison the
|
|
7
|
+
* document was never asked to support.
|
|
8
|
+
*/
|
|
9
|
+
export const STATUS_BUCKETS = ["informational", "success", "redirect", "client-error", "server-error"];
|
|
10
|
+
export function classifyStatus(status) {
|
|
11
|
+
if (status >= 100 && status < 200)
|
|
12
|
+
return "informational";
|
|
13
|
+
if (status >= 200 && status < 300)
|
|
14
|
+
return "success";
|
|
15
|
+
if (status >= 300 && status < 400)
|
|
16
|
+
return "redirect";
|
|
17
|
+
if (status >= 400 && status < 500)
|
|
18
|
+
return "client-error";
|
|
19
|
+
return "server-error";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The DTO role (`adapter.ts`'s own `roles` vocabulary: `"request"` /
|
|
23
|
+
* `"response"` / `"errorResponse"`) a bucket corresponds to — `null` for
|
|
24
|
+
* informational/redirect, which `adapter-openapi` never assigns a role to at
|
|
25
|
+
* all. Refused rather than guessed: reporting "no declared schema" for a
|
|
26
|
+
* bucket the contract was never asked about would read as a finding where
|
|
27
|
+
* there is only an unmodelled case.
|
|
28
|
+
*/
|
|
29
|
+
export function expectedRoleForBucket(bucket) {
|
|
30
|
+
if (bucket === "success")
|
|
31
|
+
return "response";
|
|
32
|
+
if (bucket === "client-error" || bucket === "server-error")
|
|
33
|
+
return "errorResponse";
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=status-bucket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-bucket.js","sourceRoot":"","sources":["../src/status-bucket.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,eAAe,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,CAAU,CAAC;AAGhH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,eAAe,CAAC;IAC1D,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,SAAS,CAAC;IACpD,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,UAAU,CAAC;IACrD,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;QAAE,OAAO,cAAc,CAAC;IACzD,OAAO,cAAc,CAAC;AACxB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAoB;IACxD,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAC5C,IAAI,MAAM,KAAK,cAAc,IAAI,MAAM,KAAK,cAAc;QAAE,OAAO,eAAe,CAAC;IACnF,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descryy/runtime-openapi-observation",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenAPI runtime contract comparison (plan §15/§18): a real observed HTTP exchange, compared against the graph's already-declared API_ENDPOINT/DTO shapes -- documented/undocumented/wrong-method resolution and status-bucket-vs-declared-role comparison.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.5"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"registry": "https://registry.npmjs.org",
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -b"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@descryy/ir": "^0.1.0",
|
|
28
|
+
"@descryy/core": "^0.1.0",
|
|
29
|
+
"@descryy/runtime-graph-correlator": "*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@descry/adapter-openapi": "file:../../../descry-adapters/packages/adapter-openapi",
|
|
33
|
+
"@descryy/runtime-controller": "*"
|
|
34
|
+
}
|
|
35
|
+
}
|