@ancplua/qyl-api-schema 4.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ no metric DTOs or routes. Profiles are not supported.
11
11
  ## Contract pipeline
12
12
 
13
13
  ```text
14
- Qyl.OpenTelemetry.SemanticConventions
14
+ Qyl.Telemetry.SemanticConventions
15
15
  |
16
16
  | generated semantic key projection
17
17
  v
@@ -45,6 +45,11 @@ regenerate the artifacts, and map the runtime model to the generated contract.
45
45
 
46
46
  ## Published artifacts
47
47
 
48
+ Both registries carry the same version from one release tag. The contract advances on
49
+ its own major cadence and does not track the qyl product version — it was already at
50
+ `5.0.0` when qyl launched at `1.0.0`. Read the current version from npm or nuget.org
51
+ rather than from prose here; this line moves faster than a README is revised.
52
+
48
53
  | Ecosystem | Artifact | Purpose |
49
54
  | --- | --- | --- |
50
55
  | npm/TypeSpec | `@ancplua/qyl-api-schema` | Authored schema for TypeSpec consumers |
@@ -54,6 +59,16 @@ regenerate the artifacts, and map the runtime model to the generated contract.
54
59
  `main.tsp` is the local compile entry point and includes emitter routing. `index.tsp`
55
60
  is the published TypeSpec entry point and contains only the client-facing contract.
56
61
 
62
+ ## Contract revision
63
+
64
+ `scripts/emit-contract-revision.mjs` stamps a deterministic revision — `sha256:` plus
65
+ the first 16 hex characters of the contract's canonical semantic digest — into both
66
+ generated faces during `npm run compile`. A collector reports the revision it was built
67
+ against on its health response, so a client can detect a peer built from a different
68
+ contract instead of discovering the mismatch one malformed field at a time. Read the
69
+ current value from a running collector or the generated artifacts; it is derived, never
70
+ hand-maintained.
71
+
57
72
  ## Develop
58
73
 
59
74
  Development and CI use Node.js 24 LTS. Published npm artifacts support maintained
@@ -0,0 +1,231 @@
1
+ import "@typespec/events";
2
+ import "@typespec/http";
3
+ import "@typespec/sse";
4
+
5
+ import "../common/errors.tsp";
6
+ import "../models/workflow.tsp";
7
+ import "./routes.tsp";
8
+
9
+ using TypeSpec.Events;
10
+ using TypeSpec.Http;
11
+ using TypeSpec.SSE;
12
+
13
+ using Qyl.Api.Contracts.Common.Errors;
14
+ using Qyl.Api.Contracts.Workflow;
15
+
16
+ namespace Qyl.Api.Contracts.Workflow;
17
+
18
+ @doc("Keep-alive emitted while a workflow event stream has no new event.")
19
+ model WorkflowHeartbeatEvent {
20
+ type: "heartbeat";
21
+
22
+ @encodedName("application/json", "high_water_mark")
23
+ highWaterMark: WorkflowSequence;
24
+
25
+ timestamp: utcDateTime;
26
+ }
27
+
28
+ @doc("Signals that the requested cursor predates retained workflow history.")
29
+ model WorkflowCursorGapEvent {
30
+ type: "cursor_gap";
31
+
32
+ @encodedName("application/json", "oldest_available_sequence")
33
+ oldestAvailableSequence: WorkflowSequence;
34
+
35
+ @encodedName("application/json", "high_water_mark")
36
+ highWaterMark: WorkflowSequence;
37
+
38
+ timestamp: utcDateTime;
39
+ }
40
+
41
+ @events
42
+ union WorkflowStreamEvents {
43
+ event: WorkflowJournalEvent,
44
+ heartbeat: WorkflowHeartbeatEvent,
45
+ cursorGap: WorkflowCursorGapEvent,
46
+ }
47
+
48
+ @route("/api/v1/workflow-runs")
49
+ @tag("Workflow runs")
50
+ @useAuth(Qyl.Api.Contracts.QylApiKeyHeaderAuth)
51
+ interface WorkflowRunsApi {
52
+ @post
53
+ create(
54
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
55
+ @body request: WorkflowRunCreateRequest,
56
+ ):
57
+ | WorkflowRun
58
+ | ValidationError
59
+ | UnauthorizedError
60
+ | ConflictError
61
+ | ServiceUnavailableError
62
+ | InternalServerError;
63
+
64
+ @get
65
+ list(
66
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
67
+ @query status?: WorkflowRunStatus,
68
+ @query
69
+ @minValue(1)
70
+ @maxValue(200)
71
+ limit?: int32 = 50,
72
+ @query cursor?: string,
73
+ ):
74
+ | WorkflowRunPage
75
+ | ValidationError
76
+ | UnauthorizedError
77
+ | ServiceUnavailableError
78
+ | InternalServerError;
79
+
80
+ @get
81
+ @route("/{run_id}")
82
+ get(
83
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
84
+ @path("run_id") runId: WorkflowRunId,
85
+ ):
86
+ | WorkflowRun
87
+ | UnauthorizedError
88
+ | NotFoundError
89
+ | ServiceUnavailableError
90
+ | InternalServerError;
91
+
92
+ @post
93
+ @route("/{run_id}/events")
94
+ appendEvents(
95
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
96
+ @path("run_id") runId: WorkflowRunId,
97
+ @body request: WorkflowEventBatchAppendRequest,
98
+ ):
99
+ | WorkflowEventBatchAppendResponse
100
+ | ValidationError
101
+ | UnauthorizedError
102
+ | NotFoundError
103
+ | ConflictError
104
+ | ServiceUnavailableError
105
+ | InternalServerError;
106
+
107
+ @get
108
+ @route("/{run_id}/events")
109
+ readEvents(
110
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
111
+ @path("run_id") runId: WorkflowRunId,
112
+ @query("after_sequence") afterSequence?: WorkflowSequence,
113
+ @query
114
+ @minValue(1)
115
+ @maxValue(1000)
116
+ limit?: int32 = 250,
117
+ @query("wait_ms")
118
+ @minValue(0)
119
+ @maxValue(30000)
120
+ waitMs?: int32 = 0,
121
+ ):
122
+ | WorkflowEventPage
123
+ | ValidationError
124
+ | UnauthorizedError
125
+ | NotFoundError
126
+ | ServiceUnavailableError
127
+ | InternalServerError;
128
+
129
+ @get
130
+ @route("/{run_id}/graph")
131
+ getGraph(
132
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
133
+ @path("run_id") runId: WorkflowRunId,
134
+ @query("node_cursor") nodeCursor?: string,
135
+ @query("node_limit")
136
+ @minValue(1)
137
+ @maxValue(1000)
138
+ nodeLimit?: int32 = 250,
139
+ @query("edge_cursor") edgeCursor?: string,
140
+ @query("edge_limit")
141
+ @minValue(1)
142
+ @maxValue(2000)
143
+ edgeLimit?: int32 = 500,
144
+ ):
145
+ | WorkflowGraphSnapshot
146
+ | ValidationError
147
+ | UnauthorizedError
148
+ | NotFoundError
149
+ | ServiceUnavailableError
150
+ | InternalServerError;
151
+
152
+ @get
153
+ @route("/{run_id}/content/{content_ref}")
154
+ getContent(
155
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
156
+ @path("run_id") runId: WorkflowRunId,
157
+ @path("content_ref") contentRef: WorkflowContentRef,
158
+ ):
159
+ | WorkflowContent
160
+ | UnauthorizedError
161
+ | NotFoundError
162
+ | ServiceUnavailableError
163
+ | InternalServerError;
164
+
165
+ @get
166
+ @route("/{run_id}/stream")
167
+ stream(
168
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
169
+ @path("run_id") runId: WorkflowRunId,
170
+ @header("Last-Event-ID") lastEventId?: string,
171
+ ):
172
+ | SSEStream<WorkflowStreamEvents>
173
+ | ValidationError
174
+ | UnauthorizedError
175
+ | NotFoundError
176
+ | ServiceUnavailableError
177
+ | InternalServerError;
178
+
179
+ @post
180
+ @route("/{run_id}/commands")
181
+ submitControl(
182
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
183
+ @path("run_id") runId: WorkflowRunId,
184
+ @body request: WorkflowControlRequest,
185
+ ):
186
+ | WorkflowControlCommand
187
+ | ValidationError
188
+ | UnauthorizedError
189
+ | NotFoundError
190
+ | ConflictError
191
+ | ServiceUnavailableError
192
+ | InternalServerError;
193
+
194
+ @get
195
+ @route("/{run_id}/commands")
196
+ pollControls(
197
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
198
+ @path("run_id") runId: WorkflowRunId,
199
+ @query("after_sequence") afterSequence?: WorkflowSequence,
200
+ @query
201
+ @minValue(1)
202
+ @maxValue(100)
203
+ limit?: int32 = 20,
204
+ @query("wait_ms")
205
+ @minValue(0)
206
+ @maxValue(30000)
207
+ waitMs?: int32 = 0,
208
+ ):
209
+ | WorkflowControlCommandPage
210
+ | ValidationError
211
+ | UnauthorizedError
212
+ | NotFoundError
213
+ | ServiceUnavailableError
214
+ | InternalServerError;
215
+
216
+ @post
217
+ @route("/{run_id}/commands/{command_id}/status")
218
+ updateControl(
219
+ ...Qyl.Api.Contracts.ProjectScopeHeader,
220
+ @path("run_id") runId: WorkflowRunId,
221
+ @path("command_id") commandId: WorkflowCommandId,
222
+ @body request: WorkflowControlStatusUpdateRequest,
223
+ ):
224
+ | WorkflowControlCommand
225
+ | ValidationError
226
+ | UnauthorizedError
227
+ | NotFoundError
228
+ | ConflictError
229
+ | ServiceUnavailableError
230
+ | InternalServerError;
231
+ }
package/common/types.tsp CHANGED
@@ -178,12 +178,11 @@ scalar Temperature extends float64;
178
178
  @maxLength(64)
179
179
  scalar Sha256Hash extends string;
180
180
 
181
- // The revision is a content hash of the emitted OpenAPI artifact, not a package
182
- // version: two builds of the same contract share it whatever their package
183
- // versions say, and any contract edit changes it without anyone remembering to
184
- // bump anything. Truncated to 16 hex characters because it is compared for
185
- // equality, never used as a security boundary.
186
- @doc("Deterministic revision of the generated API contract: 'sha256:' followed by the first 16 hex characters of the SHA-256 digest of the emitted OpenAPI document.")
181
+ // The revision is a canonical digest of the semantic OpenAPI projection and the
182
+ // exported telemetry-key projection, not a package version. Presentation-only
183
+ // fields do not change it. Truncated to 16 hex characters because it is compared
184
+ // for equality, never used as a security boundary.
185
+ @doc("Deterministic revision of the generated API contract: 'sha256:' followed by the first 16 hex characters of its canonical semantic digest.")
187
186
  @pattern("^sha256:[a-f0-9]{16}$")
188
187
  @minLength(23)
189
188
  @maxLength(23)