diff --git a/.gitignore b/.gitignore index 4e4c1bd..14e83ec 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules/ **/build/ *.xcuserstate .DS_Store +docs/superpowers diff --git a/Fixtures/v1/app-info.json b/Contract/v1/Fixtures/app-info.json similarity index 100% rename from Fixtures/v1/app-info.json rename to Contract/v1/Fixtures/app-info.json diff --git a/Fixtures/v1/handshake-request.json b/Contract/v1/Fixtures/handshake-request.json similarity index 100% rename from Fixtures/v1/handshake-request.json rename to Contract/v1/Fixtures/handshake-request.json diff --git a/Fixtures/v1/handshake-response.json b/Contract/v1/Fixtures/handshake-response.json similarity index 100% rename from Fixtures/v1/handshake-response.json rename to Contract/v1/Fixtures/handshake-response.json diff --git a/Fixtures/v1/hierarchy.json b/Contract/v1/Fixtures/hierarchy.json similarity index 100% rename from Fixtures/v1/hierarchy.json rename to Contract/v1/Fixtures/hierarchy.json diff --git a/Fixtures/v1/invalid-handshake-request.json b/Contract/v1/Fixtures/invalid-handshake-request.json similarity index 100% rename from Fixtures/v1/invalid-handshake-request.json rename to Contract/v1/Fixtures/invalid-handshake-request.json diff --git a/Fixtures/v1/node-detail.json b/Contract/v1/Fixtures/node-detail.json similarity index 100% rename from Fixtures/v1/node-detail.json rename to Contract/v1/Fixtures/node-detail.json diff --git a/PROTOCOL.md b/Contract/v1/PROTOCOL.md similarity index 100% rename from PROTOCOL.md rename to Contract/v1/PROTOCOL.md diff --git a/Schemas/v1/app-info.schema.json b/Contract/v1/Schemas/app-info.schema.json similarity index 100% rename from Schemas/v1/app-info.schema.json rename to Contract/v1/Schemas/app-info.schema.json diff --git a/Schemas/v1/handshake.schema.json b/Contract/v1/Schemas/handshake.schema.json similarity index 100% rename from Schemas/v1/handshake.schema.json rename to Contract/v1/Schemas/handshake.schema.json diff --git a/Schemas/v1/hierarchy.schema.json b/Contract/v1/Schemas/hierarchy.schema.json similarity index 100% rename from Schemas/v1/hierarchy.schema.json rename to Contract/v1/Schemas/hierarchy.schema.json diff --git a/Schemas/v1/node-detail.schema.json b/Contract/v1/Schemas/node-detail.schema.json similarity index 100% rename from Schemas/v1/node-detail.schema.json rename to Contract/v1/Schemas/node-detail.schema.json diff --git a/Fixtures/v2/invalid/attribute-identifier-not-namespaced.json b/Contract/v2/Fixtures/invalid/attribute-identifier-not-namespaced.json similarity index 100% rename from Fixtures/v2/invalid/attribute-identifier-not-namespaced.json rename to Contract/v2/Fixtures/invalid/attribute-identifier-not-namespaced.json diff --git a/Fixtures/v2/invalid/coordinate-rect-missing-unit.json b/Contract/v2/Fixtures/invalid/coordinate-rect-missing-unit.json similarity index 100% rename from Fixtures/v2/invalid/coordinate-rect-missing-unit.json rename to Contract/v2/Fixtures/invalid/coordinate-rect-missing-unit.json diff --git a/Fixtures/v2/invalid/descending-protocol-range.json b/Contract/v2/Fixtures/invalid/descending-protocol-range.json similarity index 100% rename from Fixtures/v2/invalid/descending-protocol-range.json rename to Contract/v2/Fixtures/invalid/descending-protocol-range.json diff --git a/Fixtures/v2/invalid/display-size-missing-unit.json b/Contract/v2/Fixtures/invalid/display-size-missing-unit.json similarity index 100% rename from Fixtures/v2/invalid/display-size-missing-unit.json rename to Contract/v2/Fixtures/invalid/display-size-missing-unit.json diff --git a/Fixtures/v2/invalid/extension-key-not-namespaced.json b/Contract/v2/Fixtures/invalid/extension-key-not-namespaced.json similarity index 100% rename from Fixtures/v2/invalid/extension-key-not-namespaced.json rename to Contract/v2/Fixtures/invalid/extension-key-not-namespaced.json diff --git a/Fixtures/v2/invalid/failure-response-contains-payload.json b/Contract/v2/Fixtures/invalid/failure-response-contains-payload.json similarity index 100% rename from Fixtures/v2/invalid/failure-response-contains-payload.json rename to Contract/v2/Fixtures/invalid/failure-response-contains-payload.json diff --git a/Fixtures/v2/invalid/inconsistent-visibility.json b/Contract/v2/Fixtures/invalid/inconsistent-visibility.json similarity index 100% rename from Fixtures/v2/invalid/inconsistent-visibility.json rename to Contract/v2/Fixtures/invalid/inconsistent-visibility.json diff --git a/Fixtures/v2/invalid/node-id-is-number.json b/Contract/v2/Fixtures/invalid/node-id-is-number.json similarity index 100% rename from Fixtures/v2/invalid/node-id-is-number.json rename to Contract/v2/Fixtures/invalid/node-id-is-number.json diff --git a/Fixtures/v2/invalid/patch-pattern-not-namespaced.json b/Contract/v2/Fixtures/invalid/patch-pattern-not-namespaced.json similarity index 100% rename from Fixtures/v2/invalid/patch-pattern-not-namespaced.json rename to Contract/v2/Fixtures/invalid/patch-pattern-not-namespaced.json diff --git a/Contract/v2/Fixtures/invalid/relation-type-not-namespaced.json b/Contract/v2/Fixtures/invalid/relation-type-not-namespaced.json new file mode 100644 index 0000000..c254cc2 --- /dev/null +++ b/Contract/v2/Fixtures/invalid/relation-type-not-namespaced.json @@ -0,0 +1,6 @@ +{ + "type": "backingLayer", + "sourceNodeID": "node:view:1", + "targetNodeID": "node:layer:1", + "extensions": { "vendor.graph.confidence": 1 } +} diff --git a/Fixtures/v2/invalid/request-missing-request-id.json b/Contract/v2/Fixtures/invalid/request-missing-request-id.json similarity index 100% rename from Fixtures/v2/invalid/request-missing-request-id.json rename to Contract/v2/Fixtures/invalid/request-missing-request-id.json diff --git a/Fixtures/v2/invalid/request-uses-v1.json b/Contract/v2/Fixtures/invalid/request-uses-v1.json similarity index 100% rename from Fixtures/v2/invalid/request-uses-v1.json rename to Contract/v2/Fixtures/invalid/request-uses-v1.json diff --git a/Fixtures/v2/invalid/success-response-contains-error.json b/Contract/v2/Fixtures/invalid/success-response-contains-error.json similarity index 100% rename from Fixtures/v2/invalid/success-response-contains-error.json rename to Contract/v2/Fixtures/invalid/success-response-contains-error.json diff --git a/Fixtures/v2/invalid/unknown-response-status.json b/Contract/v2/Fixtures/invalid/unknown-response-status.json similarity index 100% rename from Fixtures/v2/invalid/unknown-response-status.json rename to Contract/v2/Fixtures/invalid/unknown-response-status.json diff --git a/Fixtures/v2/invalid/unsafe-integer-attribute-value.json b/Contract/v2/Fixtures/invalid/unsafe-integer-attribute-value.json similarity index 100% rename from Fixtures/v2/invalid/unsafe-integer-attribute-value.json rename to Contract/v2/Fixtures/invalid/unsafe-integer-attribute-value.json diff --git a/Fixtures/v2/valid/application-info-request.json b/Contract/v2/Fixtures/valid/application-info-request.json similarity index 100% rename from Fixtures/v2/valid/application-info-request.json rename to Contract/v2/Fixtures/valid/application-info-request.json diff --git a/Fixtures/v2/valid/application-info-response.json b/Contract/v2/Fixtures/valid/application-info-response.json similarity index 100% rename from Fixtures/v2/valid/application-info-response.json rename to Contract/v2/Fixtures/valid/application-info-response.json diff --git a/Fixtures/v2/valid/apply-attribute-patch-request.json b/Contract/v2/Fixtures/valid/apply-attribute-patch-request.json similarity index 100% rename from Fixtures/v2/valid/apply-attribute-patch-request.json rename to Contract/v2/Fixtures/valid/apply-attribute-patch-request.json diff --git a/Fixtures/v2/valid/apply-attribute-patch-response.json b/Contract/v2/Fixtures/valid/apply-attribute-patch-response.json similarity index 100% rename from Fixtures/v2/valid/apply-attribute-patch-response.json rename to Contract/v2/Fixtures/valid/apply-attribute-patch-response.json diff --git a/Fixtures/v2/valid/cancel-request.json b/Contract/v2/Fixtures/valid/cancel-request.json similarity index 100% rename from Fixtures/v2/valid/cancel-request.json rename to Contract/v2/Fixtures/valid/cancel-request.json diff --git a/Fixtures/v2/valid/cancel-response.json b/Contract/v2/Fixtures/valid/cancel-response.json similarity index 100% rename from Fixtures/v2/valid/cancel-response.json rename to Contract/v2/Fixtures/valid/cancel-response.json diff --git a/Fixtures/v2/valid/clear-attribute-patches-request.json b/Contract/v2/Fixtures/valid/clear-attribute-patches-request.json similarity index 100% rename from Fixtures/v2/valid/clear-attribute-patches-request.json rename to Contract/v2/Fixtures/valid/clear-attribute-patches-request.json diff --git a/Fixtures/v2/valid/clear-attribute-patches-response.json b/Contract/v2/Fixtures/valid/clear-attribute-patches-response.json similarity index 100% rename from Fixtures/v2/valid/clear-attribute-patches-response.json rename to Contract/v2/Fixtures/valid/clear-attribute-patches-response.json diff --git a/Fixtures/v2/valid/handshake-request.json b/Contract/v2/Fixtures/valid/handshake-request.json similarity index 100% rename from Fixtures/v2/valid/handshake-request.json rename to Contract/v2/Fixtures/valid/handshake-request.json diff --git a/Fixtures/v2/valid/handshake-response.json b/Contract/v2/Fixtures/valid/handshake-response.json similarity index 95% rename from Fixtures/v2/valid/handshake-response.json rename to Contract/v2/Fixtures/valid/handshake-response.json index b502f97..28862af 100644 --- a/Fixtures/v2/valid/handshake-response.json +++ b/Contract/v2/Fixtures/valid/handshake-response.json @@ -18,6 +18,7 @@ "attributePatchDiscovery", "attributePatching", "requestCancellation", + "uiGraphRelations", "vendor.experimentalInspection" ] } diff --git a/Fixtures/v2/valid/hierarchy-snapshot-request.json b/Contract/v2/Fixtures/valid/hierarchy-snapshot-request.json similarity index 100% rename from Fixtures/v2/valid/hierarchy-snapshot-request.json rename to Contract/v2/Fixtures/valid/hierarchy-snapshot-request.json diff --git a/Fixtures/v2/valid/hierarchy-snapshot-response.json b/Contract/v2/Fixtures/valid/hierarchy-snapshot-response.json similarity index 95% rename from Fixtures/v2/valid/hierarchy-snapshot-response.json rename to Contract/v2/Fixtures/valid/hierarchy-snapshot-response.json index 5b65b1c..8894fff 100644 --- a/Fixtures/v2/valid/hierarchy-snapshot-response.json +++ b/Contract/v2/Fixtures/valid/hierarchy-snapshot-response.json @@ -92,6 +92,14 @@ } ] } + ], + "relations": [ + { + "type": "vendor.graph.related", + "sourceNodeID": "node:window:0", + "targetNodeID": "node:label:1", + "extensions": {} + } ] } } diff --git a/Fixtures/v2/valid/list-attribute-patches-request.json b/Contract/v2/Fixtures/valid/list-attribute-patches-request.json similarity index 100% rename from Fixtures/v2/valid/list-attribute-patches-request.json rename to Contract/v2/Fixtures/valid/list-attribute-patches-request.json diff --git a/Fixtures/v2/valid/list-attribute-patches-response.json b/Contract/v2/Fixtures/valid/list-attribute-patches-response.json similarity index 100% rename from Fixtures/v2/valid/list-attribute-patches-response.json rename to Contract/v2/Fixtures/valid/list-attribute-patches-response.json diff --git a/Fixtures/v2/valid/node-detail-failure-response.json b/Contract/v2/Fixtures/valid/node-detail-failure-response.json similarity index 100% rename from Fixtures/v2/valid/node-detail-failure-response.json rename to Contract/v2/Fixtures/valid/node-detail-failure-response.json diff --git a/Fixtures/v2/valid/node-detail-request.json b/Contract/v2/Fixtures/valid/node-detail-request.json similarity index 100% rename from Fixtures/v2/valid/node-detail-request.json rename to Contract/v2/Fixtures/valid/node-detail-request.json diff --git a/Fixtures/v2/valid/node-detail-response.json b/Contract/v2/Fixtures/valid/node-detail-response.json similarity index 100% rename from Fixtures/v2/valid/node-detail-response.json rename to Contract/v2/Fixtures/valid/node-detail-response.json diff --git a/Contract/v2/Fixtures/valid/node-relation.json b/Contract/v2/Fixtures/valid/node-relation.json new file mode 100644 index 0000000..f3ae58e --- /dev/null +++ b/Contract/v2/Fixtures/valid/node-relation.json @@ -0,0 +1,6 @@ +{ + "type": "vendor.graph.related", + "sourceNodeID": "node:view:1", + "targetNodeID": "node:layer:1", + "extensions": { "vendor.graph.confidence": 1 } +} diff --git a/Fixtures/v2/valid/patchable-attributes-request.json b/Contract/v2/Fixtures/valid/patchable-attributes-request.json similarity index 100% rename from Fixtures/v2/valid/patchable-attributes-request.json rename to Contract/v2/Fixtures/valid/patchable-attributes-request.json diff --git a/Fixtures/v2/valid/patchable-attributes-response.json b/Contract/v2/Fixtures/valid/patchable-attributes-response.json similarity index 100% rename from Fixtures/v2/valid/patchable-attributes-response.json rename to Contract/v2/Fixtures/valid/patchable-attributes-response.json diff --git a/Fixtures/v2/valid/revert-attribute-patch-request.json b/Contract/v2/Fixtures/valid/revert-attribute-patch-request.json similarity index 100% rename from Fixtures/v2/valid/revert-attribute-patch-request.json rename to Contract/v2/Fixtures/valid/revert-attribute-patch-request.json diff --git a/Fixtures/v2/valid/revert-attribute-patch-response.json b/Contract/v2/Fixtures/valid/revert-attribute-patch-response.json similarity index 100% rename from Fixtures/v2/valid/revert-attribute-patch-response.json rename to Contract/v2/Fixtures/valid/revert-attribute-patch-response.json diff --git a/Fixtures/v2/valid/unknown-method-failure-response.json b/Contract/v2/Fixtures/valid/unknown-method-failure-response.json similarity index 100% rename from Fixtures/v2/valid/unknown-method-failure-response.json rename to Contract/v2/Fixtures/valid/unknown-method-failure-response.json diff --git a/Fixtures/v2/valid/unknown-method-request.json b/Contract/v2/Fixtures/valid/unknown-method-request.json similarity index 100% rename from Fixtures/v2/valid/unknown-method-request.json rename to Contract/v2/Fixtures/valid/unknown-method-request.json diff --git a/Fixtures/v2/valid/vector-attribute-value.json b/Contract/v2/Fixtures/valid/vector-attribute-value.json similarity index 100% rename from Fixtures/v2/valid/vector-attribute-value.json rename to Contract/v2/Fixtures/valid/vector-attribute-value.json diff --git a/PROTOCOL-2.0.md b/Contract/v2/PROTOCOL.md similarity index 96% rename from PROTOCOL-2.0.md rename to Contract/v2/PROTOCOL.md index e1b5665..25b3d59 100644 --- a/PROTOCOL-2.0.md +++ b/Contract/v2/PROTOCOL.md @@ -12,7 +12,7 @@ ADB, screenshots, CLI commands, MCP tools, UIKit, Auto Layout, Android View, or Compose implementation details. A transport only needs to provide a reliable, ordered byte stream. -`PROTOCOL-2.0.md`, `Schemas/v2`, and `Fixtures/v2/manifest.json` are normative. +`Contract/v2/PROTOCOL.md`, `Contract/v2/Schemas`, and `Contract/v2/manifest.json` are normative. Swift and Kotlin source code are implementations of this contract, not its definition. @@ -201,6 +201,19 @@ Visibility keeps independent causes instead of overloading one Boolean: ancestor, has effective opacity greater than `0.01`, intersects the viewport, and is not fully clipped by an ancestor. +### 8.3 UI graph relations + +The `uiGraphRelations` capability advertises that hierarchy snapshots may +contain an optional `relations` member. Each directed relation contains an open +namespaced `type`, a `sourceNodeID`, a `targetNodeID`, and namespaced +`extensions`. Relation types remain producer-owned vocabulary rather than a +closed Protocol enum. + +The Runtime reports observed relations between captured nodes. It does not +duplicate hierarchy parent/child edges in `relations`. When either the +`uiGraphRelations` capability or the optional member is absent, the Host must +not infer that any cross-tree relation exists. + ## 9. Node details and attributes Node detail is grouped into sections. Section categories and attribute @@ -439,7 +452,7 @@ not reproduce Swift type names or Swift coding behavior. ## 15. Conformance assets -Schemas use JSON Schema Draft 2020-12. `Fixtures/v2/manifest.json` lists every +Schemas use JSON Schema Draft 2020-12. `Contract/v2/manifest.json` lists every method, Schema reference, valid Fixture, invalid Fixture, expected outcome, and rejection reason. A conforming implementation must: diff --git a/Schemas/v2/common.schema.json b/Contract/v2/Schemas/common.schema.json similarity index 100% rename from Schemas/v2/common.schema.json rename to Contract/v2/Schemas/common.schema.json diff --git a/Schemas/v2/conformance-manifest.schema.json b/Contract/v2/Schemas/conformance-manifest.schema.json similarity index 80% rename from Schemas/v2/conformance-manifest.schema.json rename to Contract/v2/Schemas/conformance-manifest.schema.json index 86225b7..e8d1745 100644 --- a/Schemas/v2/conformance-manifest.schema.json +++ b/Contract/v2/Schemas/conformance-manifest.schema.json @@ -3,12 +3,21 @@ "$id": "https://astrolabe.dev/schemas/v2/conformance-manifest.schema.json", "title": "Astrolabe Protocol 2.0 Conformance Manifest", "type": "object", - "required": ["schemaVersion", "wireProtocolVersion", "methods", "cases"], + "required": ["schemaVersion", "wireProtocolVersion", "fixtureRoots", "methods", "cases"], "properties": { - "schemaVersion": { "const": 1 }, + "schemaVersion": { "const": 2 }, "wireProtocolVersion": { "$ref": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/protocolVersion" }, + "fixtureRoots": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^(?!.*\\/\\.\\.?($|\\/))Contract/v2/Fixtures/(valid|invalid)(?:/[^/]+)*$" + } + }, "methods": { "type": "array", "minItems": 1, @@ -32,7 +41,7 @@ "required": ["name", "fixture", "schema", "expectation"], "properties": { "name": { "type": "string", "minLength": 1 }, - "fixture": { "type": "string", "pattern": "^Fixtures/v2/(valid|invalid)/.+\\.json$" }, + "fixture": { "type": "string", "pattern": "^Contract/v2/Fixtures/(valid|invalid)/.+\\.json$" }, "schema": { "type": "string", "format": "uri-reference" }, "expectation": { "enum": ["valid", "invalid"] }, "semanticRule": { "enum": ["ascendingProtocolRange", "derivedVisibility"] }, diff --git a/Schemas/v2/inspection.schema.json b/Contract/v2/Schemas/inspection.schema.json similarity index 93% rename from Schemas/v2/inspection.schema.json rename to Contract/v2/Schemas/inspection.schema.json index f867891..9cceb44 100644 --- a/Schemas/v2/inspection.schema.json +++ b/Contract/v2/Schemas/inspection.schema.json @@ -222,6 +222,24 @@ } } }, + "nodeRelation": { + "type": "object", + "required": ["type", "sourceNodeID", "targetNodeID", "extensions"], + "properties": { + "type": { + "$ref": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/namespacedIdentifier" + }, + "sourceNodeID": { + "$ref": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/opaqueIdentifier" + }, + "targetNodeID": { + "$ref": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/opaqueIdentifier" + }, + "extensions": { + "$ref": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/extensionMap" + } + } + }, "hierarchySnapshotPayload": { "type": "object", "required": [ @@ -255,6 +273,10 @@ "type": "array", "items": { "$ref": "#/$defs/node" } }, + "relations": { + "type": "array", + "items": { "$ref": "#/$defs/nodeRelation" } + }, "extensions": { "$ref": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/extensionMap" } diff --git a/Schemas/v2/patching.schema.json b/Contract/v2/Schemas/patching.schema.json similarity index 100% rename from Schemas/v2/patching.schema.json rename to Contract/v2/Schemas/patching.schema.json diff --git a/Schemas/v2/request.schema.json b/Contract/v2/Schemas/request.schema.json similarity index 100% rename from Schemas/v2/request.schema.json rename to Contract/v2/Schemas/request.schema.json diff --git a/Schemas/v2/response.schema.json b/Contract/v2/Schemas/response.schema.json similarity index 100% rename from Schemas/v2/response.schema.json rename to Contract/v2/Schemas/response.schema.json diff --git a/Schemas/v2/session.schema.json b/Contract/v2/Schemas/session.schema.json similarity index 100% rename from Schemas/v2/session.schema.json rename to Contract/v2/Schemas/session.schema.json diff --git a/Contract/v2/manifest.json b/Contract/v2/manifest.json new file mode 100644 index 0000000..9798474 --- /dev/null +++ b/Contract/v2/manifest.json @@ -0,0 +1,122 @@ +{ + "schemaVersion": 2, + "wireProtocolVersion": { "major": 2, "minor": 0 }, + "fixtureRoots": [ + "Contract/v2/Fixtures/valid", + "Contract/v2/Fixtures/invalid" + ], + "methods": [ + { + "name": "handshake", + "capability": null, + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/handshakeRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/handshakeSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/handshakeFailureResponse" + }, + { + "name": "applicationInfo", + "capability": "applicationInfo", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applicationInfoRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applicationInfoSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applicationInfoFailureResponse" + }, + { + "name": "hierarchySnapshot", + "capability": "hierarchySnapshot", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/hierarchySnapshotRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/hierarchySnapshotSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/hierarchySnapshotFailureResponse" + }, + { + "name": "nodeDetail", + "capability": "nodeDetail", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/nodeDetailRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailFailureResponse" + }, + { + "name": "patchableAttributes", + "capability": "attributePatchDiscovery", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/patchableAttributesRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/patchableAttributesSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/patchableAttributesFailureResponse" + }, + { + "name": "applyAttributePatch", + "capability": "attributePatching", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applyAttributePatchRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applyAttributePatchSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applyAttributePatchFailureResponse" + }, + { + "name": "listAttributePatches", + "capability": "attributePatching", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/listAttributePatchesRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/listAttributePatchesSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/listAttributePatchesFailureResponse" + }, + { + "name": "revertAttributePatch", + "capability": "attributePatching", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/revertAttributePatchRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/revertAttributePatchSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/revertAttributePatchFailureResponse" + }, + { + "name": "clearAttributePatches", + "capability": "attributePatching", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/clearAttributePatchesRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/clearAttributePatchesSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/clearAttributePatchesFailureResponse" + }, + { + "name": "cancelRequest", + "capability": "requestCancellation", + "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/cancelRequestRequest", + "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/cancelRequestSuccessResponse", + "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/cancelRequestFailureResponse" + } + ], + "cases": [ + { "name": "handshake request", "fixture": "Contract/v2/Fixtures/valid/handshake-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/handshakeRequest", "expectation": "valid" }, + { "name": "handshake response", "fixture": "Contract/v2/Fixtures/valid/handshake-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/handshakeSuccessResponse", "expectation": "valid" }, + { "name": "application info request", "fixture": "Contract/v2/Fixtures/valid/application-info-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applicationInfoRequest", "expectation": "valid" }, + { "name": "application info response", "fixture": "Contract/v2/Fixtures/valid/application-info-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applicationInfoSuccessResponse", "expectation": "valid" }, + { "name": "hierarchy request", "fixture": "Contract/v2/Fixtures/valid/hierarchy-snapshot-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/hierarchySnapshotRequest", "expectation": "valid" }, + { "name": "hierarchy response", "fixture": "Contract/v2/Fixtures/valid/hierarchy-snapshot-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/hierarchySnapshotSuccessResponse", "expectation": "valid" }, + { "name": "node detail request", "fixture": "Contract/v2/Fixtures/valid/node-detail-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/nodeDetailRequest", "expectation": "valid" }, + { "name": "node detail response", "fixture": "Contract/v2/Fixtures/valid/node-detail-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailSuccessResponse", "expectation": "valid" }, + { "name": "patch catalog request", "fixture": "Contract/v2/Fixtures/valid/patchable-attributes-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/patchableAttributesRequest", "expectation": "valid" }, + { "name": "patch catalog response", "fixture": "Contract/v2/Fixtures/valid/patchable-attributes-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/patchableAttributesSuccessResponse", "expectation": "valid" }, + { "name": "apply patch request", "fixture": "Contract/v2/Fixtures/valid/apply-attribute-patch-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applyAttributePatchRequest", "expectation": "valid" }, + { "name": "apply patch response", "fixture": "Contract/v2/Fixtures/valid/apply-attribute-patch-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applyAttributePatchSuccessResponse", "expectation": "valid" }, + { "name": "list patches request", "fixture": "Contract/v2/Fixtures/valid/list-attribute-patches-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/listAttributePatchesRequest", "expectation": "valid" }, + { "name": "list patches response", "fixture": "Contract/v2/Fixtures/valid/list-attribute-patches-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/listAttributePatchesSuccessResponse", "expectation": "valid" }, + { "name": "revert patch request", "fixture": "Contract/v2/Fixtures/valid/revert-attribute-patch-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/revertAttributePatchRequest", "expectation": "valid" }, + { "name": "revert patch response", "fixture": "Contract/v2/Fixtures/valid/revert-attribute-patch-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/revertAttributePatchSuccessResponse", "expectation": "valid" }, + { "name": "clear patches request", "fixture": "Contract/v2/Fixtures/valid/clear-attribute-patches-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/clearAttributePatchesRequest", "expectation": "valid" }, + { "name": "clear patches response", "fixture": "Contract/v2/Fixtures/valid/clear-attribute-patches-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/clearAttributePatchesSuccessResponse", "expectation": "valid" }, + { "name": "cancel request", "fixture": "Contract/v2/Fixtures/valid/cancel-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/cancelRequestRequest", "expectation": "valid" }, + { "name": "cancel response", "fixture": "Contract/v2/Fixtures/valid/cancel-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/cancelRequestSuccessResponse", "expectation": "valid" }, + { "name": "failure response", "fixture": "Contract/v2/Fixtures/valid/node-detail-failure-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailFailureResponse", "expectation": "valid" }, + { "name": "unknown method request envelope", "fixture": "Contract/v2/Fixtures/valid/unknown-method-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/unknownMethodRequest", "expectation": "valid" }, + { "name": "unknown method failure envelope", "fixture": "Contract/v2/Fixtures/valid/unknown-method-failure-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/unknownMethodFailureResponse", "expectation": "valid" }, + { "name": "signed vector attribute value", "fixture": "Contract/v2/Fixtures/valid/vector-attribute-value.json", "schema": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/attributeValue", "expectation": "valid" }, + { "name": "namespaced node relation", "fixture": "Contract/v2/Fixtures/valid/node-relation.json", "schema": "https://astrolabe.dev/schemas/v2/inspection.schema.json#/$defs/nodeRelation", "expectation": "valid" }, + { "name": "missing request id", "fixture": "Contract/v2/Fixtures/invalid/request-missing-request-id.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applicationInfoRequest", "expectation": "invalid", "reason": "requestID is required" }, + { "name": "v1 message on v2 contract", "fixture": "Contract/v2/Fixtures/invalid/request-uses-v1.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applicationInfoRequest", "expectation": "invalid", "reason": "V2 does not accept V1 aliases or envelopes" }, + { "name": "numeric node id", "fixture": "Contract/v2/Fixtures/invalid/node-id-is-number.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/nodeDetailRequest", "expectation": "invalid", "reason": "node IDs are opaque strings" }, + { "name": "success with error", "fixture": "Contract/v2/Fixtures/invalid/success-response-contains-error.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/cancelRequestSuccessResponse", "expectation": "invalid", "reason": "success responses cannot contain error" }, + { "name": "failure with payload", "fixture": "Contract/v2/Fixtures/invalid/failure-response-contains-payload.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailFailureResponse", "expectation": "invalid", "reason": "failure responses cannot contain payload" }, + { "name": "unnamespaced attribute", "fixture": "Contract/v2/Fixtures/invalid/attribute-identifier-not-namespaced.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applyAttributePatchRequest", "expectation": "invalid", "reason": "attribute identifiers require a namespace" }, + { "name": "unnamespaced patch pattern", "fixture": "Contract/v2/Fixtures/invalid/patch-pattern-not-namespaced.json", "schema": "https://astrolabe.dev/schemas/v2/patching.schema.json#/$defs/patchableAttribute", "expectation": "invalid", "reason": "patch catalog patterns require a namespace" }, + { "name": "unsafe integer", "fixture": "Contract/v2/Fixtures/invalid/unsafe-integer-attribute-value.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applyAttributePatchRequest", "expectation": "invalid", "reason": "integer values must remain JSON safe" }, + { "name": "coordinate without unit", "fixture": "Contract/v2/Fixtures/invalid/coordinate-rect-missing-unit.json", "schema": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/coordinateRect", "expectation": "invalid", "reason": "every coordinate declares its unit" }, + { "name": "display size without unit", "fixture": "Contract/v2/Fixtures/invalid/display-size-missing-unit.json", "schema": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/displayInfo", "expectation": "invalid", "reason": "display dimensions declare logical or pixel units" }, + { "name": "unnamespaced extension", "fixture": "Contract/v2/Fixtures/invalid/extension-key-not-namespaced.json", "schema": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/extensionMap", "expectation": "invalid", "reason": "extension keys require a namespace" }, + { "name": "unknown response status", "fixture": "Contract/v2/Fixtures/invalid/unknown-response-status.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json", "expectation": "invalid", "reason": "response status is a closed discriminator" }, + { "name": "descending protocol range", "fixture": "Contract/v2/Fixtures/invalid/descending-protocol-range.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/handshakeRequest", "expectation": "invalid", "semanticRule": "ascendingProtocolRange", "reason": "minimum protocol version cannot exceed maximum" }, + { "name": "inconsistent visibility", "fixture": "Contract/v2/Fixtures/invalid/inconsistent-visibility.json", "schema": "https://astrolabe.dev/schemas/v2/inspection.schema.json#/$defs/visibility", "expectation": "invalid", "semanticRule": "derivedVisibility", "reason": "a hidden node cannot be onscreen" }, + { "name": "relation type without namespace", "fixture": "Contract/v2/Fixtures/invalid/relation-type-not-namespaced.json", "schema": "https://astrolabe.dev/schemas/v2/inspection.schema.json#/$defs/nodeRelation", "expectation": "invalid", "reason": "relation types require a namespace" } + ] +} diff --git a/Fixtures/v2/manifest.json b/Fixtures/v2/manifest.json deleted file mode 100644 index 64d6901..0000000 --- a/Fixtures/v2/manifest.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "schemaVersion": 1, - "wireProtocolVersion": { "major": 2, "minor": 0 }, - "methods": [ - { - "name": "handshake", - "capability": null, - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/handshakeRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/handshakeSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/handshakeFailureResponse" - }, - { - "name": "applicationInfo", - "capability": "applicationInfo", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applicationInfoRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applicationInfoSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applicationInfoFailureResponse" - }, - { - "name": "hierarchySnapshot", - "capability": "hierarchySnapshot", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/hierarchySnapshotRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/hierarchySnapshotSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/hierarchySnapshotFailureResponse" - }, - { - "name": "nodeDetail", - "capability": "nodeDetail", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/nodeDetailRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailFailureResponse" - }, - { - "name": "patchableAttributes", - "capability": "attributePatchDiscovery", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/patchableAttributesRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/patchableAttributesSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/patchableAttributesFailureResponse" - }, - { - "name": "applyAttributePatch", - "capability": "attributePatching", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applyAttributePatchRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applyAttributePatchSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applyAttributePatchFailureResponse" - }, - { - "name": "listAttributePatches", - "capability": "attributePatching", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/listAttributePatchesRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/listAttributePatchesSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/listAttributePatchesFailureResponse" - }, - { - "name": "revertAttributePatch", - "capability": "attributePatching", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/revertAttributePatchRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/revertAttributePatchSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/revertAttributePatchFailureResponse" - }, - { - "name": "clearAttributePatches", - "capability": "attributePatching", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/clearAttributePatchesRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/clearAttributePatchesSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/clearAttributePatchesFailureResponse" - }, - { - "name": "cancelRequest", - "capability": "requestCancellation", - "requestSchema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/cancelRequestRequest", - "successResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/cancelRequestSuccessResponse", - "failureResponseSchema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/cancelRequestFailureResponse" - } - ], - "cases": [ - { "name": "handshake request", "fixture": "Fixtures/v2/valid/handshake-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/handshakeRequest", "expectation": "valid" }, - { "name": "handshake response", "fixture": "Fixtures/v2/valid/handshake-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/handshakeSuccessResponse", "expectation": "valid" }, - { "name": "application info request", "fixture": "Fixtures/v2/valid/application-info-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applicationInfoRequest", "expectation": "valid" }, - { "name": "application info response", "fixture": "Fixtures/v2/valid/application-info-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applicationInfoSuccessResponse", "expectation": "valid" }, - { "name": "hierarchy request", "fixture": "Fixtures/v2/valid/hierarchy-snapshot-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/hierarchySnapshotRequest", "expectation": "valid" }, - { "name": "hierarchy response", "fixture": "Fixtures/v2/valid/hierarchy-snapshot-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/hierarchySnapshotSuccessResponse", "expectation": "valid" }, - { "name": "node detail request", "fixture": "Fixtures/v2/valid/node-detail-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/nodeDetailRequest", "expectation": "valid" }, - { "name": "node detail response", "fixture": "Fixtures/v2/valid/node-detail-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailSuccessResponse", "expectation": "valid" }, - { "name": "patch catalog request", "fixture": "Fixtures/v2/valid/patchable-attributes-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/patchableAttributesRequest", "expectation": "valid" }, - { "name": "patch catalog response", "fixture": "Fixtures/v2/valid/patchable-attributes-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/patchableAttributesSuccessResponse", "expectation": "valid" }, - { "name": "apply patch request", "fixture": "Fixtures/v2/valid/apply-attribute-patch-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applyAttributePatchRequest", "expectation": "valid" }, - { "name": "apply patch response", "fixture": "Fixtures/v2/valid/apply-attribute-patch-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/applyAttributePatchSuccessResponse", "expectation": "valid" }, - { "name": "list patches request", "fixture": "Fixtures/v2/valid/list-attribute-patches-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/listAttributePatchesRequest", "expectation": "valid" }, - { "name": "list patches response", "fixture": "Fixtures/v2/valid/list-attribute-patches-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/listAttributePatchesSuccessResponse", "expectation": "valid" }, - { "name": "revert patch request", "fixture": "Fixtures/v2/valid/revert-attribute-patch-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/revertAttributePatchRequest", "expectation": "valid" }, - { "name": "revert patch response", "fixture": "Fixtures/v2/valid/revert-attribute-patch-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/revertAttributePatchSuccessResponse", "expectation": "valid" }, - { "name": "clear patches request", "fixture": "Fixtures/v2/valid/clear-attribute-patches-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/clearAttributePatchesRequest", "expectation": "valid" }, - { "name": "clear patches response", "fixture": "Fixtures/v2/valid/clear-attribute-patches-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/clearAttributePatchesSuccessResponse", "expectation": "valid" }, - { "name": "cancel request", "fixture": "Fixtures/v2/valid/cancel-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/cancelRequestRequest", "expectation": "valid" }, - { "name": "cancel response", "fixture": "Fixtures/v2/valid/cancel-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/cancelRequestSuccessResponse", "expectation": "valid" }, - { "name": "failure response", "fixture": "Fixtures/v2/valid/node-detail-failure-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailFailureResponse", "expectation": "valid" }, - { "name": "unknown method request envelope", "fixture": "Fixtures/v2/valid/unknown-method-request.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/unknownMethodRequest", "expectation": "valid" }, - { "name": "unknown method failure envelope", "fixture": "Fixtures/v2/valid/unknown-method-failure-response.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/unknownMethodFailureResponse", "expectation": "valid" }, - { "name": "signed vector attribute value", "fixture": "Fixtures/v2/valid/vector-attribute-value.json", "schema": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/attributeValue", "expectation": "valid" }, - { "name": "missing request id", "fixture": "Fixtures/v2/invalid/request-missing-request-id.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applicationInfoRequest", "expectation": "invalid", "reason": "requestID is required" }, - { "name": "v1 message on v2 contract", "fixture": "Fixtures/v2/invalid/request-uses-v1.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applicationInfoRequest", "expectation": "invalid", "reason": "V2 does not accept V1 aliases or envelopes" }, - { "name": "numeric node id", "fixture": "Fixtures/v2/invalid/node-id-is-number.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/nodeDetailRequest", "expectation": "invalid", "reason": "node IDs are opaque strings" }, - { "name": "success with error", "fixture": "Fixtures/v2/invalid/success-response-contains-error.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/cancelRequestSuccessResponse", "expectation": "invalid", "reason": "success responses cannot contain error" }, - { "name": "failure with payload", "fixture": "Fixtures/v2/invalid/failure-response-contains-payload.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json#/$defs/nodeDetailFailureResponse", "expectation": "invalid", "reason": "failure responses cannot contain payload" }, - { "name": "unnamespaced attribute", "fixture": "Fixtures/v2/invalid/attribute-identifier-not-namespaced.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applyAttributePatchRequest", "expectation": "invalid", "reason": "attribute identifiers require a namespace" }, - { "name": "unnamespaced patch pattern", "fixture": "Fixtures/v2/invalid/patch-pattern-not-namespaced.json", "schema": "https://astrolabe.dev/schemas/v2/patching.schema.json#/$defs/patchableAttribute", "expectation": "invalid", "reason": "patch catalog patterns require a namespace" }, - { "name": "unsafe integer", "fixture": "Fixtures/v2/invalid/unsafe-integer-attribute-value.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/applyAttributePatchRequest", "expectation": "invalid", "reason": "integer values must remain JSON safe" }, - { "name": "coordinate without unit", "fixture": "Fixtures/v2/invalid/coordinate-rect-missing-unit.json", "schema": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/coordinateRect", "expectation": "invalid", "reason": "every coordinate declares its unit" }, - { "name": "display size without unit", "fixture": "Fixtures/v2/invalid/display-size-missing-unit.json", "schema": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/displayInfo", "expectation": "invalid", "reason": "display dimensions declare logical or pixel units" }, - { "name": "unnamespaced extension", "fixture": "Fixtures/v2/invalid/extension-key-not-namespaced.json", "schema": "https://astrolabe.dev/schemas/v2/common.schema.json#/$defs/extensionMap", "expectation": "invalid", "reason": "extension keys require a namespace" }, - { "name": "unknown response status", "fixture": "Fixtures/v2/invalid/unknown-response-status.json", "schema": "https://astrolabe.dev/schemas/v2/response.schema.json", "expectation": "invalid", "reason": "response status is a closed discriminator" }, - { "name": "descending protocol range", "fixture": "Fixtures/v2/invalid/descending-protocol-range.json", "schema": "https://astrolabe.dev/schemas/v2/request.schema.json#/$defs/handshakeRequest", "expectation": "invalid", "semanticRule": "ascendingProtocolRange", "reason": "minimum protocol version cannot exceed maximum" }, - { "name": "inconsistent visibility", "fixture": "Fixtures/v2/invalid/inconsistent-visibility.json", "schema": "https://astrolabe.dev/schemas/v2/inspection.schema.json#/$defs/visibility", "expectation": "invalid", "semanticRule": "derivedVisibility", "reason": "a hidden node cannot be onscreen" } - ] -} diff --git a/AstrolabeProtocolKotlin/build.gradle.kts b/Implementations/Kotlin/build.gradle.kts similarity index 99% rename from AstrolabeProtocolKotlin/build.gradle.kts rename to Implementations/Kotlin/build.gradle.kts index 6b752de..2a85313 100644 --- a/AstrolabeProtocolKotlin/build.gradle.kts +++ b/Implementations/Kotlin/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { sourceSets { test { - resources.srcDir(rootProject.layout.projectDirectory.dir("Fixtures")) + resources.srcDir(rootProject.layout.projectDirectory.dir("Contract")) } } diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeAttributeValue.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/attributes/RuntimeAttributeValue.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeAttributeValue.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/attributes/RuntimeAttributeValue.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeAttributedTextRun.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/attributes/RuntimeAttributedTextRun.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeAttributedTextRun.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/attributes/RuntimeAttributedTextRun.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeLayoutRelation.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/attributes/RuntimeLayoutRelation.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeLayoutRelation.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/attributes/RuntimeLayoutRelation.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeColor.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeColor.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeColor.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeColor.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeGeometry.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeGeometry.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeGeometry.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeGeometry.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeIdentifier.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeIdentifier.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeIdentifier.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/core/RuntimeIdentifier.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/framing/RuntimeFrameCodec.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/framing/RuntimeFrameCodec.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/framing/RuntimeFrameCodec.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/framing/RuntimeFrameCodec.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplication.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplication.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplication.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplication.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchy.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchy.kt similarity index 98% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchy.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchy.kt index 7bbec1c..97afc45 100644 --- a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchy.kt +++ b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchy.kt @@ -186,6 +186,8 @@ public data class RuntimeHierarchySnapshotPayload( public val viewport: RuntimeCoordinateRect, /** Ordered hierarchy roots. */ public val roots: List, + /** Directed cross-tree relations captured with this hierarchy. */ + public val relations: List? = null, /** Optional namespaced snapshot facts. */ public val extensions: RuntimeExtensionMap? = null ) { diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetail.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetail.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetail.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetail.kt diff --git a/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeRelation.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeRelation.kt new file mode 100644 index 0000000..48594f6 --- /dev/null +++ b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeRelation.kt @@ -0,0 +1,23 @@ +// +// RuntimeNodeRelation.kt +// astrolabe-protocol +// +// Created by 轩辕十四 on 2026/8/4. +// + +package dev.astrolabe.protocol + +import kotlinx.serialization.Serializable + +/** Directed platform-neutral relation between two captured Runtime nodes. */ +@Serializable +public data class RuntimeNodeRelation( + /** Open namespaced relation type owned by its producer. */ + public val type: RuntimeNamespacedIdentifier, + /** Source node identifier for this directed relation. */ + public val sourceNodeID: RuntimeOpaqueIdentifier, + /** Target node identifier for this directed relation. */ + public val targetNodeID: RuntimeOpaqueIdentifier, + /** Namespaced producer-specific relation facts. */ + public val extensions: RuntimeExtensionMap +) diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellation.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellation.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellation.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellation.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeJsonDocumentValidator.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeJsonDocumentValidator.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeJsonDocumentValidator.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeJsonDocumentValidator.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessage.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessage.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessage.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessage.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodec.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodec.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodec.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodec.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeCapability.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeCapability.kt similarity index 90% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeCapability.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeCapability.kt index 8c1523e..3d8ea39 100644 --- a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeCapability.kt +++ b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeCapability.kt @@ -35,6 +35,8 @@ public value class RuntimeCapability( public val attributePatching: RuntimeCapability = RuntimeCapability("attributePatching") /** Runtime supports request cancellation. */ public val requestCancellation: RuntimeCapability = RuntimeCapability("requestCancellation") + /** Runtime can report directed cross-tree node relations. */ + public val uiGraphRelations: RuntimeCapability = RuntimeCapability("uiGraphRelations") } } diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeHandshake.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeHandshake.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeHandshake.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeHandshake.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeMethod.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeMethod.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeMethod.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeMethod.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeProtocolVersion.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeProtocolVersion.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeProtocolVersion.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/negotiation/RuntimeProtocolVersion.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/patching/RuntimeAttributePatch.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/patching/RuntimeAttributePatch.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/patching/RuntimeAttributePatch.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/patching/RuntimeAttributePatch.kt diff --git a/AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/patching/RuntimePatchableAttribute.kt b/Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/patching/RuntimePatchableAttribute.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/main/kotlin/dev/astrolabe/protocol/patching/RuntimePatchableAttribute.kt rename to Implementations/Kotlin/src/main/kotlin/dev/astrolabe/protocol/patching/RuntimePatchableAttribute.kt diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetailModelTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/attributes/RuntimeAttributeValueTest.kt similarity index 77% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetailModelTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/attributes/RuntimeAttributeValueTest.kt index 76f45fe..bd85d14 100644 --- a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetailModelTest.kt +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/attributes/RuntimeAttributeValueTest.kt @@ -1,5 +1,5 @@ // -// RuntimeNodeDetailModelTest.kt +// RuntimeAttributeValueTest.kt // astrolabe-protocol // // Created by 轩辕十四 on 2026/7/20. @@ -15,36 +15,13 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertIs -class RuntimeNodeDetailModelTest { +class RuntimeAttributeValueTest { private val codec = RuntimeMessageCodec() - @Test - fun `node detail fixtures decode through typed contracts`() { - val request = codec.decodeRequest(fixture("v2/valid/node-detail-request.json")) - val response = codec.decodeResponse(fixture("v2/valid/node-detail-response.json")) - - val parameters = codec.decodeRequestParameters(request, RuntimeNodeDetailParameters.contract) - val payload = codec.decodeSuccessPayload(response, RuntimeNodeDetailPayload.contract) - - assertEquals(parameters.nodeID, payload.nodeID) - assertEquals("common.text", payload.sections.first().category.rawValue) - } - - @Test - fun `node identifiers and attribute identifiers retain wire constraints`() { - assertFailsWith { - val request = codec.decodeRequest(fixture("v2/invalid/node-id-is-number.json")) - codec.decodeRequestParameters(request, RuntimeNodeDetailParameters.contract) - } - assertFailsWith { - RuntimeAttributeIdentifier("content") - } - } - @Test fun `standalone vector attribute values decode with signed components`() { val value = codec.decodeValue( - fixture("v2/valid/vector-attribute-value.json"), + fixture("v2/Fixtures/valid/vector-attribute-value.json"), RuntimeAttributeValue.serializer() ) diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/contract/ProtocolFixtureConformanceTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/contract/ProtocolFixtureConformanceTest.kt similarity index 93% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/contract/ProtocolFixtureConformanceTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/contract/ProtocolFixtureConformanceTest.kt index e45160c..4d43107 100644 --- a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/contract/ProtocolFixtureConformanceTest.kt +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/contract/ProtocolFixtureConformanceTest.kt @@ -32,6 +32,7 @@ class ProtocolFixtureConformanceTest { "hierarchy-snapshot-response.json" to { decodeResponse(RuntimeHierarchySnapshotPayload.contract, it) }, "list-attribute-patches-request.json" to { decodeRequest(RuntimeListAttributePatchesParameters.contract, it) }, "list-attribute-patches-response.json" to { decodeResponse(RuntimeAttributePatchListPayload.contract, it) }, + "node-relation.json" to { codec.decodeValue(it, RuntimeNodeRelation.serializer()) }, "node-detail-failure-response.json" to { codec.decodeResponse(it) }, "node-detail-request.json" to { decodeRequest(RuntimeNodeDetailParameters.contract, it) }, "node-detail-response.json" to { decodeResponse(RuntimeNodeDetailPayload.contract, it) }, @@ -44,8 +45,8 @@ class ProtocolFixtureConformanceTest { "vector-attribute-value.json" to { codec.decodeValue(it, RuntimeAttributeValue.serializer()) } ) - assertEquals(fixtureNames("v2/valid"), cases.keys) - cases.forEach { (name, operation) -> operation(fixture("v2/valid/$name")) } + assertEquals(fixtureNames("v2/Fixtures/valid"), cases.keys) + cases.forEach { (name, operation) -> operation(fixture("v2/Fixtures/valid/$name")) } } @Test @@ -78,6 +79,9 @@ class ProtocolFixtureConformanceTest { }, "request-missing-request-id.json" to { source -> codec.decodeRequest(source) }, "request-uses-v1.json" to { source -> codec.decodeRequest(source) }, + "relation-type-not-namespaced.json" to { source -> + codec.decodeValue(source, RuntimeNodeRelation.serializer()) + }, "success-response-contains-error.json" to { source -> codec.decodeResponse(source) }, "unknown-response-status.json" to { source -> codec.decodeResponse(source) }, "unsafe-integer-attribute-value.json" to { source -> @@ -85,10 +89,10 @@ class ProtocolFixtureConformanceTest { } ) - assertEquals(fixtureNames("v2/invalid"), cases.keys) + assertEquals(fixtureNames("v2/Fixtures/invalid"), cases.keys) cases.forEach { (name, operation) -> assertFailsWith { - operation(fixture("v2/invalid/$name")) + operation(fixture("v2/Fixtures/invalid/$name")) } } } diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/framing/RuntimeFrameCodecTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/framing/RuntimeFrameCodecTest.kt similarity index 100% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/framing/RuntimeFrameCodecTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/framing/RuntimeFrameCodecTest.kt diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplicationModelTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplicationModelTest.kt similarity index 81% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplicationModelTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplicationModelTest.kt index c4306e6..6242f9b 100644 --- a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplicationModelTest.kt +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeApplicationModelTest.kt @@ -16,8 +16,8 @@ class RuntimeApplicationModelTest { @Test fun `application info fixtures decode through typed contracts`() { - val request = codec.decodeRequest(fixture("v2/valid/application-info-request.json")) - val response = codec.decodeResponse(fixture("v2/valid/application-info-response.json")) + val request = codec.decodeRequest(fixture("v2/Fixtures/valid/application-info-request.json")) + val response = codec.decodeResponse(fixture("v2/Fixtures/valid/application-info-response.json")) codec.decodeRequestParameters(request, RuntimeApplicationInfoParameters.contract) val payload = codec.decodeSuccessPayload(response, RuntimeApplicationInfoPayload.contract) @@ -31,7 +31,7 @@ class RuntimeApplicationModelTest { fun `display facts require units`() { assertFailsWith { codec.decodeValue( - fixture("v2/invalid/display-size-missing-unit.json"), + fixture("v2/Fixtures/invalid/display-size-missing-unit.json"), RuntimeDisplayInfo.serializer() ) } diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchyModelTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchyModelTest.kt similarity index 78% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchyModelTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchyModelTest.kt index eafa4ff..c865831 100644 --- a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchyModelTest.kt +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeHierarchyModelTest.kt @@ -16,8 +16,8 @@ class RuntimeHierarchyModelTest { @Test fun `hierarchy fixtures decode through typed contracts`() { - val request = codec.decodeRequest(fixture("v2/valid/hierarchy-snapshot-request.json")) - val response = codec.decodeResponse(fixture("v2/valid/hierarchy-snapshot-response.json")) + val request = codec.decodeRequest(fixture("v2/Fixtures/valid/hierarchy-snapshot-request.json")) + val response = codec.decodeResponse(fixture("v2/Fixtures/valid/hierarchy-snapshot-response.json")) codec.decodeRequestParameters(request, RuntimeHierarchySnapshotParameters.contract) val payload = codec.decodeSuccessPayload(response, RuntimeHierarchySnapshotPayload.contract) @@ -30,13 +30,13 @@ class RuntimeHierarchyModelTest { fun `hierarchy values reject missing units and inconsistent visibility`() { assertFailsWith { codec.decodeValue( - fixture("v2/invalid/coordinate-rect-missing-unit.json"), + fixture("v2/Fixtures/invalid/coordinate-rect-missing-unit.json"), RuntimeCoordinateRect.serializer() ) } assertFailsWith { codec.decodeValue( - fixture("v2/invalid/inconsistent-visibility.json"), + fixture("v2/Fixtures/invalid/inconsistent-visibility.json"), RuntimeNodeVisibility.serializer() ) } diff --git a/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetailModelTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetailModelTest.kt new file mode 100644 index 0000000..8ecac6c --- /dev/null +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeDetailModelTest.kt @@ -0,0 +1,43 @@ +// +// RuntimeNodeDetailModelTest.kt +// astrolabe-protocol +// +// Created by 轩辕十四 on 2026/7/20. +// + +package dev.astrolabe.protocol + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class RuntimeNodeDetailModelTest { + private val codec = RuntimeMessageCodec() + + @Test + fun `node detail fixtures decode through typed contracts`() { + val request = codec.decodeRequest(fixture("v2/Fixtures/valid/node-detail-request.json")) + val response = codec.decodeResponse(fixture("v2/Fixtures/valid/node-detail-response.json")) + + val parameters = codec.decodeRequestParameters(request, RuntimeNodeDetailParameters.contract) + val payload = codec.decodeSuccessPayload(response, RuntimeNodeDetailPayload.contract) + + assertEquals(parameters.nodeID, payload.nodeID) + assertEquals("common.text", payload.sections.first().category.rawValue) + } + + @Test + fun `node identifiers and attribute identifiers retain wire constraints`() { + assertFailsWith { + val request = codec.decodeRequest(fixture("v2/Fixtures/invalid/node-id-is-number.json")) + codec.decodeRequestParameters(request, RuntimeNodeDetailParameters.contract) + } + assertFailsWith { + RuntimeAttributeIdentifier("content") + } + } + + private fun fixture(path: String): ByteArray = checkNotNull( + javaClass.classLoader.getResource(path) + ).readBytes() +} diff --git a/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeRelationTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeRelationTest.kt new file mode 100644 index 0000000..9e9ec7b --- /dev/null +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/inspection/RuntimeNodeRelationTest.kt @@ -0,0 +1,29 @@ +package dev.astrolabe.protocol + +import kotlinx.serialization.json.JsonPrimitive +import kotlin.test.Test +import kotlin.test.assertEquals + +class RuntimeNodeRelationTest { + private val codec = RuntimeMessageCodec() + + @Test + fun `namespaced node relation round trips`() { + val relation = RuntimeNodeRelation( + type = RuntimeNamespacedIdentifier("vendor.graph.related"), + sourceNodeID = RuntimeOpaqueIdentifier("node:view:1"), + targetNodeID = RuntimeOpaqueIdentifier("node:layer:1"), + extensions = RuntimeExtensionMap( + mapOf("vendor.graph.confidence" to JsonPrimitive(1)) + ) + ) + + val encoded = codec.encodeValue(relation, RuntimeNodeRelation.serializer()) + + assertEquals( + relation, + codec.decodeValue(encoded, RuntimeNodeRelation.serializer()) + ) + assertEquals("uiGraphRelations", RuntimeCapability.uiGraphRelations.rawValue) + } +} diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellationModelTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellationModelTest.kt similarity index 81% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellationModelTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellationModelTest.kt index 08c5bf5..57d6e81 100644 --- a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellationModelTest.kt +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeCancellationModelTest.kt @@ -15,8 +15,8 @@ class RuntimeCancellationModelTest { @Test fun `cancellation fixtures decode through typed contracts`() { - val request = codec.decodeRequest(fixture("v2/valid/cancel-request.json")) - val response = codec.decodeResponse(fixture("v2/valid/cancel-response.json")) + val request = codec.decodeRequest(fixture("v2/Fixtures/valid/cancel-request.json")) + val response = codec.decodeResponse(fixture("v2/Fixtures/valid/cancel-response.json")) val parameters = codec.decodeRequestParameters(request, RuntimeCancelRequestParameters.contract) val payload = codec.decodeSuccessPayload(response, RuntimeCancelRequestPayload.contract) diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodecTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodecTest.kt similarity index 82% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodecTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodecTest.kt index f2b3f1f..edd2926 100644 --- a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodecTest.kt +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/messaging/RuntimeMessageCodecTest.kt @@ -20,7 +20,7 @@ class RuntimeMessageCodecTest { @Test fun `request envelope preserves method parameters and extensions`() { - val source = fixture("v2/valid/unknown-method-request.json") + val source = fixture("v2/Fixtures/valid/unknown-method-request.json") val request = codec.decodeRequest(source) val encoded = codec.encodeRequest(request) @@ -33,23 +33,23 @@ class RuntimeMessageCodecTest { @Test fun `response envelope enforces status member exclusivity`() { - val success = codec.decodeResponse(fixture("v2/valid/handshake-response.json")) - val failure = codec.decodeResponse(fixture("v2/valid/node-detail-failure-response.json")) + val success = codec.decodeResponse(fixture("v2/Fixtures/valid/handshake-response.json")) + val failure = codec.decodeResponse(fixture("v2/Fixtures/valid/node-detail-failure-response.json")) assertIs(success.outcome) assertIs(failure.outcome) assertFailsWith { - codec.decodeResponse(fixture("v2/invalid/success-response-contains-error.json")) + codec.decodeResponse(fixture("v2/Fixtures/invalid/success-response-contains-error.json")) } assertFailsWith { - codec.decodeResponse(fixture("v2/invalid/failure-response-contains-payload.json")) + codec.decodeResponse(fixture("v2/Fixtures/invalid/failure-response-contains-payload.json")) } } @Test fun `codec rejects unsupported protocol versions and scalar roots`() { assertFailsWith { - codec.decodeRequest(fixture("v2/invalid/request-uses-v1.json")) + codec.decodeRequest(fixture("v2/Fixtures/invalid/request-uses-v1.json")) } assertFailsWith { codec.decodeRequest("[]".encodeToByteArray()) @@ -103,9 +103,9 @@ class RuntimeMessageCodecTest { @Test fun `codec decodes every valid request and response fixture`() { validFixtureNames() - .filterNot { it == "vector-attribute-value.json" } + .filterNot { it in setOf("node-relation.json", "vector-attribute-value.json") } .forEach { name -> - val source = fixture("v2/valid/$name") + val source = fixture("v2/Fixtures/valid/$name") val document = codec.decodeDocument(source) val objectValue = assertIs(document) when { @@ -118,13 +118,13 @@ class RuntimeMessageCodecTest { @Test fun `codec decodes standalone valid attribute values`() { - val document = codec.decodeDocument(fixture("v2/valid/vector-attribute-value.json")) + val document = codec.decodeDocument(fixture("v2/Fixtures/valid/vector-attribute-value.json")) assertIs(document) } private fun validFixtureNames(): List = checkNotNull( - javaClass.classLoader.getResource("v2/valid") + javaClass.classLoader.getResource("v2/Fixtures/valid") ).toURI().let(::File).listFiles() ?.filter { it.extension == "json" } ?.map { it.name } diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/negotiation/RuntimeSessionModelTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/negotiation/RuntimeSessionModelTest.kt similarity index 86% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/negotiation/RuntimeSessionModelTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/negotiation/RuntimeSessionModelTest.kt index a610be5..f15016a 100644 --- a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/negotiation/RuntimeSessionModelTest.kt +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/negotiation/RuntimeSessionModelTest.kt @@ -18,7 +18,7 @@ class RuntimeSessionModelTest { @Test fun `handshake request decodes typed parameters`() { - val request = codec.decodeRequest(fixture("v2/valid/handshake-request.json")) + val request = codec.decodeRequest(fixture("v2/Fixtures/valid/handshake-request.json")) val parameters = codec.decodeRequestParameters( request, @@ -31,7 +31,7 @@ class RuntimeSessionModelTest { @Test fun `handshake response decodes typed payload`() { - val response = codec.decodeResponse(fixture("v2/valid/handshake-response.json")) + val response = codec.decodeResponse(fixture("v2/Fixtures/valid/handshake-response.json")) val payload = codec.decodeSuccessPayload( response, @@ -45,7 +45,7 @@ class RuntimeSessionModelTest { @Test fun `typed handshake values round trip without exposing JSON assembly`() { - val sourceRequest = codec.decodeRequest(fixture("v2/valid/handshake-request.json")) + val sourceRequest = codec.decodeRequest(fixture("v2/Fixtures/valid/handshake-request.json")) val parameters = codec.decodeRequestParameters(sourceRequest, RuntimeHandshakeParameters.contract) val encodedRequest = codec.encodeRequest( requestID = sourceRequest.requestID, @@ -53,7 +53,7 @@ class RuntimeSessionModelTest { parameters = parameters ) - val sourceResponse = codec.decodeResponse(fixture("v2/valid/handshake-response.json")) + val sourceResponse = codec.decodeResponse(fixture("v2/Fixtures/valid/handshake-response.json")) val payload = codec.decodeSuccessPayload(sourceResponse, RuntimeHandshakePayload.contract) val encodedResponse = codec.encodeSuccessResponse( requestID = sourceResponse.requestID, @@ -82,7 +82,7 @@ class RuntimeSessionModelTest { ) } assertFailsWith { - codec.decodeRequest(fixture("v2/invalid/descending-protocol-range.json")).let { request -> + codec.decodeRequest(fixture("v2/Fixtures/invalid/descending-protocol-range.json")).let { request -> codec.decodeRequestParameters(request, RuntimeHandshakeParameters.contract) } } @@ -90,8 +90,8 @@ class RuntimeSessionModelTest { @Test fun `typed decoding rejects a mismatched method before reading payload`() { - val request = codec.decodeRequest(fixture("v2/valid/application-info-request.json")) - val response = codec.decodeResponse(fixture("v2/valid/application-info-response.json")) + val request = codec.decodeRequest(fixture("v2/Fixtures/valid/application-info-request.json")) + val response = codec.decodeResponse(fixture("v2/Fixtures/valid/application-info-response.json")) assertFailsWith { codec.decodeRequestParameters(request, RuntimeHandshakeParameters.contract) @@ -119,7 +119,7 @@ class RuntimeSessionModelTest { } assertFailsWith { codec.decodeValue( - fixture("v2/invalid/extension-key-not-namespaced.json"), + fixture("v2/Fixtures/invalid/extension-key-not-namespaced.json"), RuntimeExtensionMap.serializer() ) } diff --git a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/patching/RuntimePatchingModelTest.kt b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/patching/RuntimePatchingModelTest.kt similarity index 85% rename from AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/patching/RuntimePatchingModelTest.kt rename to Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/patching/RuntimePatchingModelTest.kt index de47e2b..f9591ec 100644 --- a/AstrolabeProtocolKotlin/src/test/kotlin/dev/astrolabe/protocol/patching/RuntimePatchingModelTest.kt +++ b/Implementations/Kotlin/src/test/kotlin/dev/astrolabe/protocol/patching/RuntimePatchingModelTest.kt @@ -45,20 +45,20 @@ class RuntimePatchingModelTest { @Test fun `patch identifiers and patterns reject ambiguous values`() { - val request = codec.decodeRequest(fixture("v2/invalid/attribute-identifier-not-namespaced.json")) + val request = codec.decodeRequest(fixture("v2/Fixtures/invalid/attribute-identifier-not-namespaced.json")) assertFailsWith { codec.decodeRequestParameters(request, RuntimeApplyAttributePatchParameters.contract) } assertFailsWith { codec.decodeValue( - fixture("v2/invalid/patch-pattern-not-namespaced.json"), + fixture("v2/Fixtures/invalid/patch-pattern-not-namespaced.json"), RuntimePatchableAttribute.serializer() ) } assertFailsWith { val unsafeRequest = codec.decodeRequest( - fixture("v2/invalid/unsafe-integer-attribute-value.json") + fixture("v2/Fixtures/invalid/unsafe-integer-attribute-value.json") ) codec.decodeRequestParameters(unsafeRequest, RuntimeApplyAttributePatchParameters.contract) } @@ -76,8 +76,8 @@ class RuntimePatchingModelTest { requestContract: RuntimeMethodContract, payloadContract: RuntimeMethodContract ) { - val request = codec.decodeRequest(fixture("v2/valid/$fixturePrefix-request.json")) - val response = codec.decodeResponse(fixture("v2/valid/$fixturePrefix-response.json")) + val request = codec.decodeRequest(fixture("v2/Fixtures/valid/$fixturePrefix-request.json")) + val response = codec.decodeResponse(fixture("v2/Fixtures/valid/$fixturePrefix-response.json")) codec.decodeRequestParameters(request, requestContract) codec.decodeSuccessPayload(response, payloadContract) diff --git a/Sources/AstrolabeProtocol/Inspection/RuntimeAttributeValue.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Attributes/RuntimeAttributeValue.swift similarity index 100% rename from Sources/AstrolabeProtocol/Inspection/RuntimeAttributeValue.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Attributes/RuntimeAttributeValue.swift diff --git a/Sources/AstrolabeProtocol/Inspection/RuntimeAttributedTextRun.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Attributes/RuntimeAttributedTextRun.swift similarity index 100% rename from Sources/AstrolabeProtocol/Inspection/RuntimeAttributedTextRun.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Attributes/RuntimeAttributedTextRun.swift diff --git a/Sources/AstrolabeProtocol/Inspection/RuntimeLayoutRelation.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Attributes/RuntimeLayoutRelation.swift similarity index 100% rename from Sources/AstrolabeProtocol/Inspection/RuntimeLayoutRelation.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Attributes/RuntimeLayoutRelation.swift diff --git a/Sources/AstrolabeProtocol/Core/RuntimeColor.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeColor.swift similarity index 100% rename from Sources/AstrolabeProtocol/Core/RuntimeColor.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeColor.swift diff --git a/Sources/AstrolabeProtocol/Core/RuntimeGeometry.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeGeometry.swift similarity index 100% rename from Sources/AstrolabeProtocol/Core/RuntimeGeometry.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeGeometry.swift diff --git a/Sources/AstrolabeProtocol/Core/RuntimeIdentifier.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeIdentifier.swift similarity index 100% rename from Sources/AstrolabeProtocol/Core/RuntimeIdentifier.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeIdentifier.swift diff --git a/Sources/AstrolabeProtocol/Core/RuntimeJSONValue.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeJSONValue.swift similarity index 100% rename from Sources/AstrolabeProtocol/Core/RuntimeJSONValue.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeJSONValue.swift diff --git a/Sources/AstrolabeProtocol/Core/RuntimeProtocolMetadata.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeProtocolMetadata.swift similarity index 87% rename from Sources/AstrolabeProtocol/Core/RuntimeProtocolMetadata.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeProtocolMetadata.swift index 42bfb57..deed87f 100644 --- a/Sources/AstrolabeProtocol/Core/RuntimeProtocolMetadata.swift +++ b/Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeProtocolMetadata.swift @@ -7,7 +7,7 @@ public enum RuntimeProtocolMetadata { /// AstrolabeProtocol package release version. - public static let packageVersion = "2.0.0" + public static let packageVersion = "2.1.0" /// Wire protocol version implemented by this package. public static let wireVersion = RuntimeProtocolVersion.v2 diff --git a/Sources/AstrolabeProtocol/Core/RuntimeRequiredNullable.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeRequiredNullable.swift similarity index 100% rename from Sources/AstrolabeProtocol/Core/RuntimeRequiredNullable.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeRequiredNullable.swift diff --git a/Sources/AstrolabeProtocol/Framing/RuntimeFrameCodec.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Framing/RuntimeFrameCodec.swift similarity index 100% rename from Sources/AstrolabeProtocol/Framing/RuntimeFrameCodec.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Framing/RuntimeFrameCodec.swift diff --git a/Sources/AstrolabeProtocol/Inspection/RuntimeApplication.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Inspection/Application/RuntimeApplication.swift similarity index 100% rename from Sources/AstrolabeProtocol/Inspection/RuntimeApplication.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Inspection/Application/RuntimeApplication.swift diff --git a/Sources/AstrolabeProtocol/Inspection/RuntimeHierarchy.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Inspection/Hierarchy/RuntimeHierarchy.swift similarity index 98% rename from Sources/AstrolabeProtocol/Inspection/RuntimeHierarchy.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Inspection/Hierarchy/RuntimeHierarchy.swift index 032e750..1cb18c6 100644 --- a/Sources/AstrolabeProtocol/Inspection/RuntimeHierarchy.swift +++ b/Implementations/Swift/Sources/AstrolabeProtocol/Inspection/Hierarchy/RuntimeHierarchy.swift @@ -268,6 +268,9 @@ public struct RuntimeHierarchySnapshotPayload: Codable, Equatable, Sendable, Run /// Ordered hierarchy roots. public let roots: [RuntimeNode] + /// Directed cross-tree relations captured with this hierarchy. + public let relations: [RuntimeNodeRelation]? + /// Optional namespaced snapshot facts. public let extensions: RuntimeExtensionMap? @@ -279,6 +282,7 @@ public struct RuntimeHierarchySnapshotPayload: Codable, Equatable, Sendable, Run display: RuntimeDisplayInfo, viewport: RuntimeCoordinateRect, roots: [RuntimeNode], + relations: [RuntimeNodeRelation]? = nil, extensions: RuntimeExtensionMap? = nil ) { self.snapshotID = snapshotID @@ -288,6 +292,7 @@ public struct RuntimeHierarchySnapshotPayload: Codable, Equatable, Sendable, Run self.display = display self.viewport = viewport self.roots = roots + self.relations = relations self.extensions = extensions } diff --git a/Implementations/Swift/Sources/AstrolabeProtocol/Inspection/Hierarchy/RuntimeNodeRelation.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Inspection/Hierarchy/RuntimeNodeRelation.swift new file mode 100644 index 0000000..004a795 --- /dev/null +++ b/Implementations/Swift/Sources/AstrolabeProtocol/Inspection/Hierarchy/RuntimeNodeRelation.swift @@ -0,0 +1,33 @@ +// +// RuntimeNodeRelation.swift +// astrolabe-protocol +// +// Created by 轩辕十四 on 2026/8/4. +// + +/// Directed platform-neutral relation between two captured Runtime nodes. +public struct RuntimeNodeRelation: Codable, Equatable, Sendable { + /// Open namespaced relation type owned by its producer. + public let type: RuntimeNamespacedIdentifier + + /// Source node identifier for this directed relation. + public let sourceNodeID: RuntimeOpaqueIdentifier + + /// Target node identifier for this directed relation. + public let targetNodeID: RuntimeOpaqueIdentifier + + /// Namespaced producer-specific relation facts. + public let extensions: RuntimeExtensionMap + + public init( + type: RuntimeNamespacedIdentifier, + sourceNodeID: RuntimeOpaqueIdentifier, + targetNodeID: RuntimeOpaqueIdentifier, + extensions: RuntimeExtensionMap + ) { + self.type = type + self.sourceNodeID = sourceNodeID + self.targetNodeID = targetNodeID + self.extensions = extensions + } +} diff --git a/Sources/AstrolabeProtocol/Inspection/RuntimeNodeDetail.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Inspection/NodeDetail/RuntimeNodeDetail.swift similarity index 100% rename from Sources/AstrolabeProtocol/Inspection/RuntimeNodeDetail.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Inspection/NodeDetail/RuntimeNodeDetail.swift diff --git a/Sources/AstrolabeProtocol/Messaging/RuntimeError.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Messaging/RuntimeError.swift similarity index 100% rename from Sources/AstrolabeProtocol/Messaging/RuntimeError.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Messaging/RuntimeError.swift diff --git a/Sources/AstrolabeProtocol/Framing/RuntimeMessageCodec.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Messaging/RuntimeMessageCodec.swift similarity index 100% rename from Sources/AstrolabeProtocol/Framing/RuntimeMessageCodec.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Messaging/RuntimeMessageCodec.swift diff --git a/Sources/AstrolabeProtocol/Messaging/RuntimeRequest.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Messaging/RuntimeRequest.swift similarity index 100% rename from Sources/AstrolabeProtocol/Messaging/RuntimeRequest.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Messaging/RuntimeRequest.swift diff --git a/Sources/AstrolabeProtocol/Messaging/RuntimeResponse.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Messaging/RuntimeResponse.swift similarity index 100% rename from Sources/AstrolabeProtocol/Messaging/RuntimeResponse.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Messaging/RuntimeResponse.swift diff --git a/Sources/AstrolabeProtocol/Negotiation/RuntimeCapability.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeCapability.swift similarity index 93% rename from Sources/AstrolabeProtocol/Negotiation/RuntimeCapability.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeCapability.swift index 52c1c3f..0e3295c 100644 --- a/Sources/AstrolabeProtocol/Negotiation/RuntimeCapability.swift +++ b/Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeCapability.swift @@ -32,6 +32,7 @@ public struct RuntimeCapability: Codable, Equatable, Hashable, Sendable { public static let attributePatchDiscovery = RuntimeCapability(uncheckedRawValue: "attributePatchDiscovery") public static let attributePatching = RuntimeCapability(uncheckedRawValue: "attributePatching") public static let requestCancellation = RuntimeCapability(uncheckedRawValue: "requestCancellation") + public static let uiGraphRelations = RuntimeCapability(uncheckedRawValue: "uiGraphRelations") private init(uncheckedRawValue: String) { rawValue = uncheckedRawValue diff --git a/Sources/AstrolabeProtocol/Negotiation/RuntimeHandshake.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeHandshake.swift similarity index 100% rename from Sources/AstrolabeProtocol/Negotiation/RuntimeHandshake.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeHandshake.swift diff --git a/Sources/AstrolabeProtocol/Negotiation/RuntimeMethod.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeMethod.swift similarity index 100% rename from Sources/AstrolabeProtocol/Negotiation/RuntimeMethod.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeMethod.swift diff --git a/Sources/AstrolabeProtocol/Negotiation/RuntimeProtocolVersion.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeProtocolVersion.swift similarity index 100% rename from Sources/AstrolabeProtocol/Negotiation/RuntimeProtocolVersion.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Negotiation/RuntimeProtocolVersion.swift diff --git a/Sources/AstrolabeProtocol/Patching/RuntimeAttributePatch.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Patching/RuntimeAttributePatch.swift similarity index 100% rename from Sources/AstrolabeProtocol/Patching/RuntimeAttributePatch.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Patching/RuntimeAttributePatch.swift diff --git a/Sources/AstrolabeProtocol/Patching/RuntimePatchableAttribute.swift b/Implementations/Swift/Sources/AstrolabeProtocol/Patching/RuntimePatchableAttribute.swift similarity index 100% rename from Sources/AstrolabeProtocol/Patching/RuntimePatchableAttribute.swift rename to Implementations/Swift/Sources/AstrolabeProtocol/Patching/RuntimePatchableAttribute.swift diff --git a/Tests/AstrolabeProtocolTests/Inspection/RuntimeModelTests.swift b/Implementations/Swift/Tests/AstrolabeProtocolTests/Attributes/RuntimeAttributeValueTests.swift similarity index 60% rename from Tests/AstrolabeProtocolTests/Inspection/RuntimeModelTests.swift rename to Implementations/Swift/Tests/AstrolabeProtocolTests/Attributes/RuntimeAttributeValueTests.swift index 9abc056..8de5211 100644 --- a/Tests/AstrolabeProtocolTests/Inspection/RuntimeModelTests.swift +++ b/Implementations/Swift/Tests/AstrolabeProtocolTests/Attributes/RuntimeAttributeValueTests.swift @@ -1,15 +1,14 @@ // -// RuntimeModelTests.swift +// RuntimeAttributeValueTests.swift // astrolabe-protocol // // Created by 轩辕十四 on 2026/7/16. // import AstrolabeProtocol -import Foundation import XCTest -final class RuntimeModelTests: XCTestCase { +final class RuntimeAttributeValueTests: XCTestCase { func testTypedAttributeValuesRoundTrip() throws { let values: [RuntimeAttributeValue] = [ .null, @@ -41,28 +40,4 @@ final class RuntimeModelTests: XCTestCase { ) } } - - func testProtocolRangeRejectsDescendingAndCrossMajorRanges() { - XCTAssertThrowsError( - try RuntimeProtocolRange( - minimum: RuntimeProtocolVersion(major: 2, minor: 1), - maximum: RuntimeProtocolVersion(major: 2, minor: 0) - ) - ) - XCTAssertThrowsError( - try RuntimeProtocolRange( - minimum: RuntimeProtocolVersion(major: 1, minor: 9), - maximum: RuntimeProtocolVersion(major: 2, minor: 0) - ) - ) - } - - func testOpaqueIdentifiersEncodeAsStrings() throws { - let identifier = try RuntimeOpaqueIdentifier(rawValue: "node:label:1") - let payload = try RuntimeMessageCodec().encode( - RuntimeJSONObject(values: ["nodeID": .string(identifier.rawValue)]) - ) - - XCTAssertTrue(String(decoding: payload, as: UTF8.self).contains(#""node:label:1""#)) - } } diff --git a/Tests/AstrolabeProtocolTests/Contract/ProtocolFixtureTests.swift b/Implementations/Swift/Tests/AstrolabeProtocolTests/Contract/ProtocolFixtureTests.swift similarity index 98% rename from Tests/AstrolabeProtocolTests/Contract/ProtocolFixtureTests.swift rename to Implementations/Swift/Tests/AstrolabeProtocolTests/Contract/ProtocolFixtureTests.swift index dbfa76d..dc563c1 100644 --- a/Tests/AstrolabeProtocolTests/Contract/ProtocolFixtureTests.swift +++ b/Implementations/Swift/Tests/AstrolabeProtocolTests/Contract/ProtocolFixtureTests.swift @@ -112,6 +112,8 @@ final class ProtocolFixtureTests: XCTestCase { return try wrap(RuntimeExtensionMap.self, data, codec) case "visibility": return try wrap(RuntimeNodeVisibility.self, data, codec) + case "nodeRelation": + return try wrap(RuntimeNodeRelation.self, data, codec) case "response.schema.json": return try wrap(RuntimeResponse.self, data, codec) default: @@ -137,7 +139,7 @@ final class ProtocolFixtureTests: XCTestCase { private func manifestCases(expectation: String) throws -> [ManifestCase] { let data = try Data(contentsOf: repositoryRootURL() - .appendingPathComponent("Fixtures/v2/manifest.json")) + .appendingPathComponent("Contract/v2/manifest.json")) return try JSONDecoder().decode(Manifest.self, from: data).cases.filter { $0.expectation == expectation } diff --git a/Tests/AstrolabeProtocolTests/Contract/ProtocolV2ContractTests.swift b/Implementations/Swift/Tests/AstrolabeProtocolTests/Contract/ProtocolV2ContractTests.swift similarity index 93% rename from Tests/AstrolabeProtocolTests/Contract/ProtocolV2ContractTests.swift rename to Implementations/Swift/Tests/AstrolabeProtocolTests/Contract/ProtocolV2ContractTests.swift index e6876c5..eee88c9 100644 --- a/Tests/AstrolabeProtocolTests/Contract/ProtocolV2ContractTests.swift +++ b/Implementations/Swift/Tests/AstrolabeProtocolTests/Contract/ProtocolV2ContractTests.swift @@ -12,7 +12,7 @@ final class ProtocolV2ContractTests: XCTestCase { func testV2ContractAssetsCoverEveryMethod() throws { let repositoryURL = try repositoryRootURL() let manifest = try jsonObject( - at: repositoryURL.appendingPathComponent("Fixtures/v2/manifest.json") + at: repositoryURL.appendingPathComponent("Contract/v2/manifest.json") ) let methods = try XCTUnwrap(manifest["methods"] as? [[String: Any]]) let methodNames = Set(try methods.map { method in @@ -23,7 +23,7 @@ final class ProtocolV2ContractTests: XCTestCase { XCTAssertEqual(methodNames, Set(Self.expectedMethods)) XCTAssertTrue( FileManager.default.fileExists( - atPath: repositoryURL.appendingPathComponent("PROTOCOL-2.0.md").path + atPath: repositoryURL.appendingPathComponent("Contract/v2/PROTOCOL.md").path ) ) @@ -38,7 +38,7 @@ final class ProtocolV2ContractTests: XCTestCase { func testV2ConformanceCasesReferenceExistingFixturesAndSchemas() throws { let repositoryURL = try repositoryRootURL() let manifest = try jsonObject( - at: repositoryURL.appendingPathComponent("Fixtures/v2/manifest.json") + at: repositoryURL.appendingPathComponent("Contract/v2/manifest.json") ) let cases = try XCTUnwrap(manifest["cases"] as? [[String: Any]]) let expectations = Set(cases.compactMap { $0["expectation"] as? String }) @@ -70,7 +70,7 @@ final class ProtocolV2ContractTests: XCTestCase { XCTAssertTrue( FileManager.default.fileExists( atPath: repositoryURL - .appendingPathComponent("Schemas/v2") + .appendingPathComponent("Contract/v2/Schemas") .appendingPathComponent(fileName) .path ), diff --git a/Implementations/Swift/Tests/AstrolabeProtocolTests/Core/RuntimeIdentifierTests.swift b/Implementations/Swift/Tests/AstrolabeProtocolTests/Core/RuntimeIdentifierTests.swift new file mode 100644 index 0000000..a60adb1 --- /dev/null +++ b/Implementations/Swift/Tests/AstrolabeProtocolTests/Core/RuntimeIdentifierTests.swift @@ -0,0 +1,21 @@ +// +// RuntimeIdentifierTests.swift +// astrolabe-protocol +// +// Created by 轩辕十四 on 2026/7/16. +// + +import AstrolabeProtocol +import Foundation +import XCTest + +final class RuntimeIdentifierTests: XCTestCase { + func testOpaqueIdentifiersEncodeAsStrings() throws { + let identifier = try RuntimeOpaqueIdentifier(rawValue: "node:label:1") + let payload = try RuntimeMessageCodec().encode( + RuntimeJSONObject(values: ["nodeID": .string(identifier.rawValue)]) + ) + + XCTAssertTrue(String(decoding: payload, as: UTF8.self).contains(#""node:label:1""#)) + } +} diff --git a/Tests/AstrolabeProtocolTests/Framing/RuntimeFrameCodecTests.swift b/Implementations/Swift/Tests/AstrolabeProtocolTests/Framing/RuntimeFrameCodecTests.swift similarity index 100% rename from Tests/AstrolabeProtocolTests/Framing/RuntimeFrameCodecTests.swift rename to Implementations/Swift/Tests/AstrolabeProtocolTests/Framing/RuntimeFrameCodecTests.swift diff --git a/Implementations/Swift/Tests/AstrolabeProtocolTests/Inspection/RuntimeNodeRelationTests.swift b/Implementations/Swift/Tests/AstrolabeProtocolTests/Inspection/RuntimeNodeRelationTests.swift new file mode 100644 index 0000000..526fcf5 --- /dev/null +++ b/Implementations/Swift/Tests/AstrolabeProtocolTests/Inspection/RuntimeNodeRelationTests.swift @@ -0,0 +1,22 @@ +import AstrolabeProtocol +import XCTest + +final class RuntimeNodeRelationTests: XCTestCase { + func testNamespacedNodeRelationRoundTrips() throws { + let relation = RuntimeNodeRelation( + type: try RuntimeNamespacedIdentifier(rawValue: "vendor.graph.related"), + sourceNodeID: try RuntimeOpaqueIdentifier(rawValue: "node:view:1"), + targetNodeID: try RuntimeOpaqueIdentifier(rawValue: "node:layer:1"), + extensions: try RuntimeExtensionMap( + values: ["vendor.graph.confidence": .integer(1)] + ) + ) + let codec = RuntimeMessageCodec() + + XCTAssertEqual( + relation, + try codec.decode(RuntimeNodeRelation.self, from: codec.encode(relation)) + ) + XCTAssertEqual(RuntimeCapability.uiGraphRelations.rawValue, "uiGraphRelations") + } +} diff --git a/Tests/AstrolabeProtocolTests/Messaging/RuntimeMessageCodecTests.swift b/Implementations/Swift/Tests/AstrolabeProtocolTests/Messaging/RuntimeMessageCodecTests.swift similarity index 100% rename from Tests/AstrolabeProtocolTests/Messaging/RuntimeMessageCodecTests.swift rename to Implementations/Swift/Tests/AstrolabeProtocolTests/Messaging/RuntimeMessageCodecTests.swift diff --git a/Implementations/Swift/Tests/AstrolabeProtocolTests/Negotiation/RuntimeProtocolRangeTests.swift b/Implementations/Swift/Tests/AstrolabeProtocolTests/Negotiation/RuntimeProtocolRangeTests.swift new file mode 100644 index 0000000..90615fb --- /dev/null +++ b/Implementations/Swift/Tests/AstrolabeProtocolTests/Negotiation/RuntimeProtocolRangeTests.swift @@ -0,0 +1,26 @@ +// +// RuntimeProtocolRangeTests.swift +// astrolabe-protocol +// +// Created by 轩辕十四 on 2026/7/16. +// + +import AstrolabeProtocol +import XCTest + +final class RuntimeProtocolRangeTests: XCTestCase { + func testProtocolRangeRejectsDescendingAndCrossMajorRanges() { + XCTAssertThrowsError( + try RuntimeProtocolRange( + minimum: RuntimeProtocolVersion(major: 2, minor: 1), + maximum: RuntimeProtocolVersion(major: 2, minor: 0) + ) + ) + XCTAssertThrowsError( + try RuntimeProtocolRange( + minimum: RuntimeProtocolVersion(major: 1, minor: 9), + maximum: RuntimeProtocolVersion(major: 2, minor: 0) + ) + ) + } +} diff --git a/Package.swift b/Package.swift index 25cdbaf..a8293a6 100644 --- a/Package.swift +++ b/Package.swift @@ -15,10 +15,14 @@ let package = Package( ) ], targets: [ - .target(name: "AstrolabeProtocol"), + .target( + name: "AstrolabeProtocol", + path: "Implementations/Swift/Sources/AstrolabeProtocol" + ), .testTarget( name: "AstrolabeProtocolTests", - dependencies: ["AstrolabeProtocol"] + dependencies: ["AstrolabeProtocol"], + path: "Implementations/Swift/Tests/AstrolabeProtocolTests" ) ] ) diff --git a/README.md b/README.md index edf6c5f..b76d2c0 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,15 @@ Astrolabe Host and platform Runtime SDKs. ## Contents -- Swift DTOs and typed protocol models in the `AstrolabeProtocol` product. +- The normative, versioned wire contract under `Contract/`. +- Equal Swift and Kotlin implementations under `Implementations/`. - Request and response envelopes, errors, version negotiation, and frame codecs. -- Versioned JSON Schemas under `Schemas/`. -- Valid and invalid cross-language examples under `Fixtures/`. +- Versioned JSON Schemas and cross-language Fixtures under each Contract version. - The Wire Protocol 2.0 specification in - [PROTOCOL-2.0.md](PROTOCOL-2.0.md). -- Archived Wire Protocol 1.0 documentation in [PROTOCOL.md](PROTOCOL.md). + [Contract/v2/PROTOCOL.md](Contract/v2/PROTOCOL.md). +- Archived Wire Protocol 1.0 documentation in + [Contract/v1/PROTOCOL.md](Contract/v1/PROTOCOL.md). UIKit, Android View, transport listeners, device discovery, screenshots, CLI commands, and MCP tools are outside this repository. @@ -26,7 +27,7 @@ Add the package through Swift Package Manager: ```swift .package( url: "https://github.com/regulusleow/astrolabe-protocol.git", - exact: "2.0.0" + exact: "2.1.0" ) ``` @@ -45,6 +46,32 @@ Swift types are one implementation of the contract. Other implementations use the specification, Schemas, Fixtures, and documented wire behavior as their compatibility source of truth. +## UI Graph Relations + +Runtimes advertising `uiGraphRelations` may include an optional `relations` +array in hierarchy snapshots. Each directed relation uses an open namespaced +type plus source and target node IDs. Hierarchy parent/child edges remain in the +authoritative trees and are not duplicated in this array. + +## Repository Layout + +```text +Contract/ Normative protocol documents, Schemas, and Fixtures +Implementations/ Equal Swift and Kotlin protocol implementations +Tooling/ Contract validation, release automation, and tests +``` + +Root SwiftPM, Gradle, and npm manifests are ecosystem entrypoints. They map to +the implementation and Tooling directories without making one language the +repository's primary implementation. + +Both language implementations use the same conceptual domains: `Core`, +`Attributes`, `Framing`, `Messaging`, `Negotiation`, `Inspection`, and +`Patching`. Swift uses nested `Application`, `Hierarchy`, and `NodeDetail` +directories inside Inspection; Kotlin keeps the flat public package +`dev.astrolabe.protocol` and does not manufacture package boundaries for +physical directory symmetry. + ## Development Install the contract validator and run all checks: @@ -60,6 +87,10 @@ Validation compiles every Draft 2020-12 Schema, checks valid and invalid Fixtures, applies semantic rules that JSON Schema cannot express, and verifies that the Swift DTOs accept and reject the same payloads. +`Contract/v2/manifest.json` declares recursive Fixture roots through +`fixtureRoots`. Every JSON Fixture below those roots must have exactly one +registration in `cases`. + ## License Astrolabe Protocol is available under the [Apache License 2.0](LICENSE). diff --git a/README.zh-CN.md b/README.zh-CN.md index e3f9c23..04acdb7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -6,12 +6,12 @@ Astrolabe Protocol 定义 Astrolabe Host 与各平台 Runtime SDK 共享的平 ## 内容 -- `AstrolabeProtocol` Product 中的 Swift DTO 和强类型协议模型。 +- `Contract/` 下版本化的规范 Wire Contract。 +- `Implementations/` 下地位对等的 Swift 与 Kotlin 实现。 - 请求与响应封装、错误、版本协商和帧编解码器。 -- `Schemas/` 下带版本的 JSON Schema。 -- `Fixtures/` 下用于跨语言验证的有效和无效示例。 -- [PROTOCOL-2.0.md](PROTOCOL-2.0.md) 中的 Wire Protocol 2.0 规范。 -- [PROTOCOL.md](PROTOCOL.md) 中归档的 Wire Protocol 1.0 文档。 +- 每个 Contract 版本下的 JSON Schema 与跨语言 Fixture。 +- [Contract/v2/PROTOCOL.md](Contract/v2/PROTOCOL.md) 中的 Wire Protocol 2.0 规范。 +- [Contract/v1/PROTOCOL.md](Contract/v1/PROTOCOL.md) 中归档的 Wire Protocol 1.0 文档。 UIKit、Android View、Transport Listener、设备发现、截图、CLI 命令和 MCP Tools 均不属于本仓库。 @@ -22,7 +22,7 @@ UIKit、Android View、Transport Listener、设备发现、截图、CLI 命令 ```swift .package( url: "https://github.com/regulusleow/astrolabe-protocol.git", - exact: "2.0.0" + exact: "2.1.0" ) ``` @@ -40,6 +40,28 @@ Wire Protocol 版本为 `2.0`。 Swift 类型只是该协议的一种实现。其他语言的实现应以规范、Schema、Fixture 和文档约定的 Wire 行为作为兼容性事实源。 +## UI Graph 关系 + +声明 `uiGraphRelations` capability 的 Runtime 可以在 hierarchy snapshot 中返回可选的 +`relations` 数组。每条有向关系使用开放的 namespaced type 以及源、目标 node ID。层级父子边仍由 +权威树承载,不在该数组中重复存储。 + +## 仓库目录 + +```text +Contract/ 规范协议文档、Schema 和 Fixture +Implementations/ 地位对等的 Swift 与 Kotlin 协议实现 +Tooling/ Contract 校验、发布自动化和测试 +``` + +根目录的 SwiftPM、Gradle 和 npm 清单只是各生态的标准入口,通过显式路径映射到实现与 Tooling, +不表示任一语言是仓库的主实现。 + +双端实现使用相同的概念分域:`Core`、`Attributes`、`Framing`、`Messaging`、`Negotiation`、 +`Inspection` 和 `Patching`。Swift 在 Inspection 内继续按 `Application`、`Hierarchy` 和 +`NodeDetail` 建立子目录;Kotlin 保持公开 package `dev.astrolabe.protocol` 扁平,不为追求物理目录 +对称而制造新的 package 边界。 + ## 开发 安装协议校验器并运行全部检查: @@ -54,6 +76,9 @@ swift build -c release 校验流程会编译全部 Draft 2020-12 Schema,检查有效和无效 Fixture,执行 JSON Schema 无法表达的 语义规则,并验证 Swift DTO 对相同 Payload 的接受和拒绝行为保持一致。 +`Contract/v2/manifest.json` 通过 `fixtureRoots` 声明递归 Fixture 根目录。根目录下的每个 JSON +Fixture 都必须在 `cases` 中且仅有一条注册记录。 + ## 许可证 Astrolabe Protocol 使用 [Apache License 2.0](LICENSE) 许可。 diff --git a/Tooling/ContractValidation/fixture-discovery.mjs b/Tooling/ContractValidation/fixture-discovery.mjs new file mode 100644 index 0000000..bb383cd --- /dev/null +++ b/Tooling/ContractValidation/fixture-discovery.mjs @@ -0,0 +1,76 @@ +import { lstatSync, readdirSync } from "node:fs"; +import { isAbsolute, join, relative, resolve, sep } from "node:path"; + +export function discoverFixturePaths(repositoryRoot, fixtureRoots) { + const resolvedRepositoryRoot = resolve(repositoryRoot); + const fixturePaths = []; + const discoveredPaths = new Set(); + + for (const fixtureRoot of fixtureRoots) { + const resolvedFixtureRoot = resolve(resolvedRepositoryRoot, fixtureRoot); + assertWithinRepository(resolvedRepositoryRoot, resolvedFixtureRoot, fixtureRoot); + const rootStatus = fixtureRootStatus(resolvedFixtureRoot, fixtureRoot); + if (rootStatus.isSymbolicLink()) { + throw new Error(`Fixture discovery does not allow symbolic links: ${fixtureRoot}`); + } + if (!rootStatus.isDirectory()) { + throw new Error(`Fixture root is not a directory: ${fixtureRoot}`); + } + visitDirectory( + resolvedFixtureRoot, + resolvedRepositoryRoot, + fixturePaths, + discoveredPaths + ); + } + + return fixturePaths.sort(); +} + +function visitDirectory(directory, repositoryRoot, fixturePaths, discoveredPaths) { + const entries = readdirSync(directory, { withFileTypes: true }); + for (const entry of entries) { + const path = join(directory, entry.name); + if (entry.isSymbolicLink()) { + throw new Error( + `Fixture discovery does not allow symbolic links: ${repositoryRelativePath(repositoryRoot, path)}` + ); + } + if (entry.isDirectory()) { + visitDirectory(path, repositoryRoot, fixturePaths, discoveredPaths); + } else if (entry.isFile() && entry.name.endsWith(".json")) { + const fixturePath = repositoryRelativePath(repositoryRoot, path); + if (discoveredPaths.has(fixturePath)) { + throw new Error(`Fixture discovered through multiple roots: ${fixturePath}`); + } + discoveredPaths.add(fixturePath); + fixturePaths.push(fixturePath); + } + } +} + +function fixtureRootStatus(path, displayPath) { + try { + return lstatSync(path); + } catch (error) { + if (error?.code === "ENOENT") { + throw new Error(`Fixture root does not exist: ${displayPath}`); + } + throw error; + } +} + +function assertWithinRepository(repositoryRoot, path, displayPath) { + const repositoryRelative = relative(repositoryRoot, path); + if ( + repositoryRelative === ".." || + repositoryRelative.startsWith(`..${sep}`) || + isAbsolute(repositoryRelative) + ) { + throw new Error(`Fixture root resolves outside repository: ${displayPath}`); + } +} + +function repositoryRelativePath(repositoryRoot, path) { + return relative(repositoryRoot, path).split(sep).join("/"); +} diff --git a/scripts/validate-contract.mjs b/Tooling/ContractValidation/validate-contract.mjs similarity index 87% rename from scripts/validate-contract.mjs rename to Tooling/ContractValidation/validate-contract.mjs index 54feb27..1458eeb 100644 --- a/scripts/validate-contract.mjs +++ b/Tooling/ContractValidation/validate-contract.mjs @@ -1,14 +1,15 @@ import { readFileSync, readdirSync } from "node:fs"; -import { dirname, join, relative, resolve } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; -const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const schemaDirectory = join(repositoryRoot, "Schemas/v2"); -const fixtureDirectory = join(repositoryRoot, "Fixtures/v2"); -const manifest = readJSON(join(fixtureDirectory, "manifest.json")); +import { discoverFixturePaths } from "./fixture-discovery.mjs"; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const schemaDirectory = join(repositoryRoot, "Contract/v2/Schemas"); +const manifest = readJSON(join(repositoryRoot, "Contract/v2/manifest.json")); const ajv = new Ajv2020({ allErrors: true, strict: true, allowUnionTypes: true }); addFormats(ajv); @@ -92,16 +93,22 @@ for (const method of manifest.methods) { } } -const fixtureFiles = ["valid", "invalid"].flatMap((directory) => - readdirSync(join(fixtureDirectory, directory)) - .filter((name) => name.endsWith(".json")) - .map((name) => relative(repositoryRoot, join(fixtureDirectory, directory, name))) -); +const fixtureFiles = discoverFixturePaths(repositoryRoot, manifest.fixtureRoots); +const discoveredFixtures = new Set(fixtureFiles); const unmanifestedFixtures = fixtureFiles.filter((path) => !manifestedFixtures.has(path)); if (unmanifestedFixtures.length > 0) { throw new Error(`Unregistered V2 fixtures found: ${unmanifestedFixtures.join(", ")}`); } +const undiscoveredFixtures = [...manifestedFixtures] + .filter((path) => !discoveredFixtures.has(path)) + .sort(); +if (undiscoveredFixtures.length > 0) { + throw new Error( + `Registered V2 fixtures are outside fixtureRoots: ${undiscoveredFixtures.join(", ")}` + ); +} + function assertSchemaExists(reference) { const validate = ajv.getSchema(reference); if (!validate) { diff --git a/scripts/release-prepare.mjs b/Tooling/Release/release-prepare.mjs similarity index 98% rename from scripts/release-prepare.mjs rename to Tooling/Release/release-prepare.mjs index 3b0a300..7b5a04d 100644 --- a/scripts/release-prepare.mjs +++ b/Tooling/Release/release-prepare.mjs @@ -12,7 +12,7 @@ import { } from "./versioning.mjs"; const scriptPath = fileURLToPath(import.meta.url); -const defaultProjectRoot = resolve(dirname(scriptPath), ".."); +const defaultProjectRoot = resolve(dirname(scriptPath), "../.."); export function parseReleaseArgs(args) { if (args.length !== 1) { diff --git a/scripts/version-sync.mjs b/Tooling/Release/version-sync.mjs similarity index 95% rename from scripts/version-sync.mjs rename to Tooling/Release/version-sync.mjs index ed98b6b..65d954f 100644 --- a/scripts/version-sync.mjs +++ b/Tooling/Release/version-sync.mjs @@ -10,7 +10,7 @@ import { } from "./versioning.mjs"; const scriptPath = fileURLToPath(import.meta.url); -const projectRoot = resolve(dirname(scriptPath), ".."); +const projectRoot = resolve(dirname(scriptPath), "../.."); export function parseVersionSyncArgs(args) { if (args.length === 1 && args[0] === "--check") return { mode: "check" }; diff --git a/scripts/versioning.mjs b/Tooling/Release/versioning.mjs similarity index 98% rename from scripts/versioning.mjs rename to Tooling/Release/versioning.mjs index 2cf8031..94edc8a 100644 --- a/scripts/versioning.mjs +++ b/Tooling/Release/versioning.mjs @@ -1,7 +1,7 @@ import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -const metadataPath = "Sources/AstrolabeProtocol/Core/RuntimeProtocolMetadata.swift"; +const metadataPath = "Implementations/Swift/Sources/AstrolabeProtocol/Core/RuntimeProtocolMetadata.swift"; const gradlePropertiesPath = "gradle.properties"; export const versionedPaths = Object.freeze([ diff --git a/test/ci-workflow.test.mjs b/Tooling/Tests/ci-workflow.test.mjs similarity index 100% rename from test/ci-workflow.test.mjs rename to Tooling/Tests/ci-workflow.test.mjs diff --git a/Tooling/Tests/fixture-discovery.test.mjs b/Tooling/Tests/fixture-discovery.test.mjs new file mode 100644 index 0000000..6145997 --- /dev/null +++ b/Tooling/Tests/fixture-discovery.test.mjs @@ -0,0 +1,118 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { + mkdirSync, + mkdtempSync, + rmSync, + symlinkSync, + writeFileSync +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +import { discoverFixturePaths } from "../ContractValidation/fixture-discovery.mjs"; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const validatorPath = join( + repositoryRoot, + "Tooling/ContractValidation/validate-contract.mjs" +); + +test("contract validation discovers nested unregistered fixtures", () => { + const fixtureDirectory = mkdtempSync( + join(repositoryRoot, "Contract/v2/Fixtures/valid/fixture-discovery-test-") + ); + const fixturePath = join(fixtureDirectory, "unregistered.json"); + + try { + mkdirSync(fixtureDirectory, { recursive: true }); + writeFileSync(fixturePath, "{}\n"); + + const result = spawnSync(process.execPath, [validatorPath], { + cwd: repositoryRoot, + encoding: "utf8" + }); + + assert.notEqual(result.status, 0); + assert.match( + `${result.stdout}\n${result.stderr}`, + /Unregistered V2 fixtures found: .*fixture-discovery-test-[^/]+\/unregistered\.json/ + ); + } finally { + rmSync(fixtureDirectory, { recursive: true, force: true }); + } +}); + +test("fixture discovery returns nested JSON paths in deterministic order", (context) => { + const root = temporaryRepository(context); + mkdirSync(join(root, "fixtures/nested"), { recursive: true }); + writeFileSync(join(root, "fixtures/z.json"), "{}\n"); + writeFileSync(join(root, "fixtures/nested/a.json"), "{}\n"); + writeFileSync(join(root, "fixtures/nested/ignored.txt"), "ignored\n"); + + assert.deepEqual(discoverFixturePaths(root, ["fixtures"]), [ + "fixtures/nested/a.json", + "fixtures/z.json" + ]); +}); + +test("fixture discovery rejects symbolic links", (context) => { + const root = temporaryRepository(context); + mkdirSync(join(root, "fixtures"), { recursive: true }); + writeFileSync(join(root, "target.json"), "{}\n"); + symlinkSync(join(root, "target.json"), join(root, "fixtures/linked.json")); + + assert.throws( + () => discoverFixturePaths(root, ["fixtures"]), + /Fixture discovery does not allow symbolic links: fixtures\/linked\.json/ + ); +}); + +test("fixture discovery rejects overlapping roots", (context) => { + const root = temporaryRepository(context); + mkdirSync(join(root, "fixtures/nested"), { recursive: true }); + writeFileSync(join(root, "fixtures/nested/value.json"), "{}\n"); + + assert.throws( + () => discoverFixturePaths(root, ["fixtures", "fixtures/nested"]), + /Fixture discovered through multiple roots: fixtures\/nested\/value\.json/ + ); +}); + +test("fixture discovery rejects roots outside the repository", (context) => { + const root = temporaryRepository(context); + const outsideRoot = temporaryRepository(context); + mkdirSync(join(outsideRoot, "fixtures"), { recursive: true }); + + assert.throws( + () => discoverFixturePaths(root, [join(outsideRoot, "fixtures")]), + /Fixture root resolves outside repository/ + ); +}); + +test("fixture discovery reports missing roots", (context) => { + const root = temporaryRepository(context); + + assert.throws( + () => discoverFixturePaths(root, ["missing"]), + /Fixture root does not exist: missing/ + ); +}); + +test("fixture discovery rejects non-directory roots", (context) => { + const root = temporaryRepository(context); + writeFileSync(join(root, "fixture.json"), "{}\n"); + + assert.throws( + () => discoverFixturePaths(root, ["fixture.json"]), + /Fixture root is not a directory: fixture\.json/ + ); +}); + +function temporaryRepository(context) { + const root = mkdtempSync(join(tmpdir(), "astrolabe-fixture-discovery-")); + context.after(() => rmSync(root, { recursive: true, force: true })); + return root; +} diff --git a/test/release-prepare.test.mjs b/Tooling/Tests/release-prepare.test.mjs similarity index 92% rename from test/release-prepare.test.mjs rename to Tooling/Tests/release-prepare.test.mjs index 759a356..0ab81db 100644 --- a/test/release-prepare.test.mjs +++ b/Tooling/Tests/release-prepare.test.mjs @@ -4,8 +4,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { parseReleaseArgs, prepareRelease } from "../scripts/release-prepare.mjs"; -import { versionedPaths } from "../scripts/versioning.mjs"; +import { parseReleaseArgs, prepareRelease } from "../Release/release-prepare.mjs"; +import { versionedPaths } from "../Release/versioning.mjs"; test("release preparation accepts one prerelease SemVer", () => { assert.deepEqual(parseReleaseArgs(["2.0.0-rc.2"]), { version: "2.0.0-rc.2" }); @@ -37,7 +37,10 @@ test("release preparation synchronizes, verifies, commits, and tags a prerelease '.package(url: "example", exact: "2.0.0-rc.1")\n' ); writeFileSync(join(projectRoot, "gradle.properties"), "astrolabeVersion=2.0.0-rc.1\n"); - const sourceDirectory = join(projectRoot, "Sources/AstrolabeProtocol/Core"); + const sourceDirectory = join( + projectRoot, + "Implementations/Swift/Sources/AstrolabeProtocol/Core" + ); mkdirSync(sourceDirectory, { recursive: true }); writeFileSync( join(sourceDirectory, "RuntimeProtocolMetadata.swift"), diff --git a/test/versioning.test.mjs b/Tooling/Tests/versioning.test.mjs similarity index 96% rename from test/versioning.test.mjs rename to Tooling/Tests/versioning.test.mjs index 0d45559..665377f 100644 --- a/test/versioning.test.mjs +++ b/Tooling/Tests/versioning.test.mjs @@ -4,7 +4,7 @@ import test from "node:test"; import { assertReleaseVersion, compareReleaseVersions -} from "../scripts/versioning.mjs"; +} from "../Release/versioning.mjs"; test("accepts stable and prerelease SemVer values", () => { assert.equal(assertReleaseVersion("2.0.0"), "2.0.0"); diff --git a/gradle.properties b/gradle.properties index 2de6cde..c51a78d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -astrolabeVersion=2.0.0 +astrolabeVersion=2.1.0 org.gradle.configuration-cache=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlin.code.style=official diff --git a/package-lock.json b/package-lock.json index c20b648..f9d4f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "astrolabe-protocol-contract", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "astrolabe-protocol-contract", - "version": "2.0.0", + "version": "2.1.0", "devDependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1" diff --git a/package.json b/package.json index 3594f09..a037463 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "astrolabe-protocol-contract", - "version": "2.0.0", + "version": "2.1.0", "private": true, "type": "module", "scripts": { - "test:schema": "node scripts/validate-contract.mjs", + "test:schema": "node Tooling/ContractValidation/validate-contract.mjs", "test:swift-contract": "swift test --filter ProtocolFixtureTests", - "version:check": "node scripts/version-sync.mjs --check", - "version:set": "node scripts/version-sync.mjs --set", - "release:prepare": "node scripts/release-prepare.mjs", - "test": "npm run version:check && npm run test:schema && npm run test:swift-contract && node --test test/*.test.mjs" + "version:check": "node Tooling/Release/version-sync.mjs --check", + "version:set": "node Tooling/Release/version-sync.mjs --set", + "release:prepare": "node Tooling/Release/release-prepare.mjs", + "test": "npm run version:check && npm run test:schema && npm run test:swift-contract && node --test Tooling/Tests/*.test.mjs" }, "devDependencies": { "ajv": "8.20.0", diff --git a/settings.gradle.kts b/settings.gradle.kts index d6f5cd0..55fbfdd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,3 +14,4 @@ dependencyResolutionManagement { rootProject.name = "astrolabe-protocol" include(":AstrolabeProtocolKotlin") +project(":AstrolabeProtocolKotlin").projectDir = file("Implementations/Kotlin")