@opensoftware/contracts 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +105 -2
  3. package/dist/breaking.d.ts +27 -0
  4. package/dist/breaking.js +156 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +68 -0
  7. package/dist/formats.d.ts +10 -0
  8. package/dist/formats.js +18 -0
  9. package/dist/generated/types.d.ts +543 -0
  10. package/dist/generated/types.js +1 -0
  11. package/dist/index.d.ts +28 -0
  12. package/dist/index.js +73 -0
  13. package/dist/roundtrip.d.ts +3 -0
  14. package/dist/roundtrip.js +89 -0
  15. package/events/asyncapi.yaml +144 -0
  16. package/events/gate-decided/gate-decided.schema.json +122 -0
  17. package/events/gate-requested/gate-requested.schema.json +135 -0
  18. package/events/pr-opened/pr-opened.schema.json +138 -0
  19. package/events/run-completed/run-completed.schema.json +163 -0
  20. package/events/score-recorded/score-recorded.schema.json +163 -0
  21. package/events/work-received/work-received.schema.json +182 -0
  22. package/evidence-bundle/evidence-bundle.schema.json +180 -0
  23. package/factory-definition/factory-definition.schema.json +371 -0
  24. package/openapi/openapi.json +1553 -0
  25. package/package.json +50 -3
  26. package/run-record/run-record.schema.json +179 -0
  27. package/score/score.schema.json +110 -0
  28. package/typespec/common.tsp +62 -0
  29. package/typespec/events.tsp +134 -0
  30. package/typespec/evidence-bundle.tsp +99 -0
  31. package/typespec/factory-definition.tsp +193 -0
  32. package/typespec/main.tsp +10 -0
  33. package/typespec/run-record.tsp +88 -0
  34. package/typespec/score.tsp +68 -0
  35. package/typespec/work-item.tsp +65 -0
  36. package/work-item/work-item.schema.json +129 -0
@@ -0,0 +1,543 @@
1
+ export type paths = Record<string, never>;
2
+ export type webhooks = Record<string, never>;
3
+ export type components = {
4
+ schemas: {
5
+ /** @description A person or trigger. RUNTIME-008: every run is attributed to one. */
6
+ Actor: {
7
+ kind: components["schemas"]["ActorKind"];
8
+ /** @description Stable ID in the source system, prefixed with the source, for example "slack:U024BE7LH". */
9
+ id: string;
10
+ /** @description Shown to people, for example "Varun". */
11
+ displayName?: string;
12
+ };
13
+ /**
14
+ * @description Who caused something: a person, or an automated trigger such as a cron rule or an alert.
15
+ * @enum {string}
16
+ */
17
+ ActorKind: "person" | "trigger";
18
+ Agent: {
19
+ description?: string;
20
+ /** @description Harness adapter, declared under `providers.harness`, for example "claude-code". */
21
+ harness: components["schemas"]["Name"];
22
+ /** @description A model ID or a routing alias of the inference gateway, for example "routes/triage". */
23
+ model: string;
24
+ /** @description Path to the Markdown prompt. */
25
+ prompt: string;
26
+ /** @description Repositories the agent operates on, as clone URLs or "<owner>/<name>". */
27
+ repositories?: string[];
28
+ /** @description MCP servers: `registry:<name>@<version>` or `<name>@<version>`. */
29
+ mcp?: string[];
30
+ /** @description Paths to skill folders. */
31
+ skills?: string[];
32
+ secrets?: components["schemas"]["SecretReference"][];
33
+ /** @description Runtime of the agent. */
34
+ sandbox?: components["schemas"]["Sandbox"];
35
+ };
36
+ Artifact: {
37
+ path: components["schemas"]["BundlePath"];
38
+ kind: components["schemas"]["ArtifactKind"];
39
+ /** @description IANA media type, for example "image/png". */
40
+ mediaType: string;
41
+ sha256: components["schemas"]["Sha256"];
42
+ bytes?: number;
43
+ };
44
+ /** @enum {string} */
45
+ ArtifactKind: "screenshot" | "video" | "playwright-trace" | "junit" | "log" | "report" | "other";
46
+ /**
47
+ * @description Starts the workflow, or the named job, on an event (`on`) or a schedule (`cron`). Exactly one of the two; a
48
+ * schedule names its job. One model rather than a union, so every generated client reads it without guessing.
49
+ */
50
+ Automation: {
51
+ /** @description Event filter, for example "github.issue.labeled(auto-implement)" or "sentry.fatal". */
52
+ on?: string;
53
+ /** @description Five-field cron expression in UTC. */
54
+ cron?: string;
55
+ /** @description Job to run. Required with `cron`; without it an event starts the workflow. */
56
+ run?: components["schemas"]["Name"];
57
+ } & (unknown | unknown);
58
+ Benchmark: {
59
+ /** @description Path to the Harbor task suite. */
60
+ suite: string;
61
+ /** @description Five-field cron expression in UTC. Without it the suite runs on request. */
62
+ schedule?: string;
63
+ };
64
+ /** @description A path inside the bundle: relative, forward slashes, no "..". */
65
+ BundlePath: string;
66
+ /** @description A Git commit hash, abbreviated (7+) or full. The factory version is the commit of the factory definition. */
67
+ CommitHash: string;
68
+ /**
69
+ * @description `evidence.json` at the root of an evidence bundle: what was verified, against which scenario IDs, with which
70
+ * artifacts and result (P10).
71
+ */
72
+ EvidenceManifest: {
73
+ /** @description Version of this format. */
74
+ manifestVersion: components["schemas"]["FormatVersion"];
75
+ id: components["schemas"]["Identifier"];
76
+ /** Format: date-time */
77
+ createdAt: string;
78
+ /** @description The change that was verified. */
79
+ subject: components["schemas"]["EvidenceSubject"];
80
+ runId?: components["schemas"]["Identifier"];
81
+ workItemId?: components["schemas"]["Identifier"];
82
+ /** @description The tool that wrote the bundle, for example proof 1.2.0. */
83
+ producer: components["schemas"]["Producer"];
84
+ /** @description Passed only when every scenario passed. */
85
+ result: components["schemas"]["VerificationResult"];
86
+ scenarios: components["schemas"]["ScenarioEvidence"][];
87
+ artifacts: components["schemas"]["Artifact"][];
88
+ };
89
+ /** @enum {string} */
90
+ EvidenceRequirement: "required" | "optional";
91
+ EvidenceSubject: {
92
+ /** Format: uri */
93
+ repository: string;
94
+ commit: components["schemas"]["CommitHash"];
95
+ /** Format: uri */
96
+ pullRequest?: string;
97
+ };
98
+ /**
99
+ * @description `factory.yaml`: the desired state of one factory (P3). Paths are relative to the file.
100
+ * Realizes SCN-FACTORY-CODE-001: agents and prompts; MCP servers, skills and secrets per agent;
101
+ * repositories; model and runtime; automations; scorers and benchmarks.
102
+ */
103
+ FactoryDefinition: {
104
+ /** @description Editors may point at this schema. Ignored by every tool. */
105
+ $schema?: string;
106
+ /** @description Version of this format. A new major version of the format gets a new value. */
107
+ version: components["schemas"]["FormatVersion"];
108
+ /** @description Name of the factory. */
109
+ factory: components["schemas"]["Name"];
110
+ description?: string;
111
+ /** @description Adapters per port with a version range, pinned in `factory.lock` by `factory plan`. */
112
+ providers?: components["schemas"]["Providers"];
113
+ /** @description Agents by name. */
114
+ agents: Record<string, never>;
115
+ /** @description Stages in order. Each stage runs one agent; the agent defaults to the stage name. */
116
+ workflow?: components["schemas"]["Stage"][];
117
+ /** @description What starts work without a person asking. */
118
+ automations?: components["schemas"]["Automation"][];
119
+ /** @description Scorers by name. */
120
+ scorers?: Record<string, never>;
121
+ /** @description Benchmark suites by name. */
122
+ benchmarks?: Record<string, never>;
123
+ };
124
+ /**
125
+ * @description The version of a format inside its documents. Only 1 exists; a new major version of a format adds the next.
126
+ * @constant
127
+ */
128
+ FormatVersion: 1;
129
+ Gate: {
130
+ /** @description Path to the Rego policy that decides or picks the approvers. */
131
+ policy: string;
132
+ /** @description Approver groups. The policy may narrow them. */
133
+ approvers?: components["schemas"]["Name"][];
134
+ };
135
+ /** @description gatekeeper: a gate was decided. */
136
+ GateDecided: {
137
+ /** @constant */
138
+ specversion: "1.0";
139
+ /** @description Unique per source. A consumer that sees the same source and id twice handles the event once. */
140
+ id: components["schemas"]["Identifier"];
141
+ /** @description The producing module, for example "/osf/intake". */
142
+ source: string;
143
+ /** @constant */
144
+ type: "gate.decided@v1";
145
+ /** Format: date-time */
146
+ time: string;
147
+ /** @description The ID of the thing the event is about: work item, gate, run, score or pull request. */
148
+ subject?: string;
149
+ /** @constant */
150
+ datacontenttype?: "application/json";
151
+ /** Format: uri */
152
+ dataschema?: string;
153
+ /** @description W3C trace context (CloudEvents distributed tracing extension). */
154
+ traceparent?: string;
155
+ data: components["schemas"]["GateDecidedData"];
156
+ };
157
+ GateDecidedData: {
158
+ gateId: components["schemas"]["Identifier"];
159
+ workItemId: components["schemas"]["Identifier"];
160
+ decision: components["schemas"]["GateDecision"];
161
+ /** @description The person who decided. Absent only when the gate expired. */
162
+ decidedBy?: components["schemas"]["Actor"];
163
+ /** Format: date-time */
164
+ decidedAt: string;
165
+ reason?: string;
166
+ };
167
+ /** @enum {string} */
168
+ GateDecision: "approved" | "rejected" | "expired";
169
+ /** @description gatekeeper: a decision is needed before a stage starts. */
170
+ GateRequested: {
171
+ /** @constant */
172
+ specversion: "1.0";
173
+ /** @description Unique per source. A consumer that sees the same source and id twice handles the event once. */
174
+ id: components["schemas"]["Identifier"];
175
+ /** @description The producing module, for example "/osf/intake". */
176
+ source: string;
177
+ /** @constant */
178
+ type: "gate.requested@v1";
179
+ /** Format: date-time */
180
+ time: string;
181
+ /** @description The ID of the thing the event is about: work item, gate, run, score or pull request. */
182
+ subject?: string;
183
+ /** @constant */
184
+ datacontenttype?: "application/json";
185
+ /** Format: uri */
186
+ dataschema?: string;
187
+ /** @description W3C trace context (CloudEvents distributed tracing extension). */
188
+ traceparent?: string;
189
+ data: components["schemas"]["GateRequestedData"];
190
+ };
191
+ GateRequestedData: {
192
+ gateId: components["schemas"]["Identifier"];
193
+ workItemId: components["schemas"]["Identifier"];
194
+ runId?: components["schemas"]["Identifier"];
195
+ /** @description The stage that waits. */
196
+ stage: components["schemas"]["Name"];
197
+ /** @description Path to the policy in the factory definition. */
198
+ policy: string;
199
+ /** @description Approver groups picked by the policy. */
200
+ approvers: components["schemas"]["Name"][];
201
+ /** @description What the approver is asked to decide, shown in the channel. */
202
+ summary: string;
203
+ links?: components["schemas"]["Link"][];
204
+ /** Format: date-time */
205
+ expiresAt?: string;
206
+ };
207
+ /** @description An identifier chosen by the producer. ULIDs are recommended; consumers treat it as opaque. */
208
+ Identifier: string;
209
+ Link: {
210
+ /** Format: uri */
211
+ url: string;
212
+ title?: string;
213
+ };
214
+ /** @description A lowercase name: letters, digits and hyphens, starting with a letter. Agents, stages, adapters, sources. */
215
+ Name: string;
216
+ Origin: {
217
+ /** @description The channel or system, for example "slack", "github", "sentry", "cron", "mcp". */
218
+ source: components["schemas"]["Name"];
219
+ /**
220
+ * Format: uri
221
+ * @description Link back to the origin, for example the Slack thread. Absent for a schedule.
222
+ */
223
+ url?: string;
224
+ /** @description ID in the source system. */
225
+ externalId?: string;
226
+ };
227
+ /** @description forge adapters: the factory opened a pull request. */
228
+ PrOpened: {
229
+ /** @constant */
230
+ specversion: "1.0";
231
+ /** @description Unique per source. A consumer that sees the same source and id twice handles the event once. */
232
+ id: components["schemas"]["Identifier"];
233
+ /** @description The producing module, for example "/osf/intake". */
234
+ source: string;
235
+ /** @constant */
236
+ type: "pr.opened@v1";
237
+ /** Format: date-time */
238
+ time: string;
239
+ /** @description The ID of the thing the event is about: work item, gate, run, score or pull request. */
240
+ subject?: string;
241
+ /** @constant */
242
+ datacontenttype?: "application/json";
243
+ /** Format: uri */
244
+ dataschema?: string;
245
+ /** @description W3C trace context (CloudEvents distributed tracing extension). */
246
+ traceparent?: string;
247
+ data: components["schemas"]["PrOpenedData"];
248
+ };
249
+ PrOpenedData: {
250
+ /** Format: uri */
251
+ url: string;
252
+ /** Format: uri */
253
+ repository: string;
254
+ number: number;
255
+ title: string;
256
+ headBranch: string;
257
+ baseBranch: string;
258
+ headCommit: components["schemas"]["CommitHash"];
259
+ author: components["schemas"]["Actor"];
260
+ workItemId?: components["schemas"]["Identifier"];
261
+ runId?: components["schemas"]["Identifier"];
262
+ };
263
+ /** @enum {string} */
264
+ Priority: "urgent" | "high" | "normal" | "low";
265
+ Producer: {
266
+ name: string;
267
+ version: string;
268
+ };
269
+ /** @description The adapters a factory uses, per port (docs/architecture/internals.md). Keys are adapter names. */
270
+ Providers: {
271
+ harness?: Record<string, never>;
272
+ sandbox?: Record<string, never>;
273
+ channel?: Record<string, never>;
274
+ tracker?: Record<string, never>;
275
+ forge?: Record<string, never>;
276
+ inference?: Record<string, never>;
277
+ secrets?: Record<string, never>;
278
+ telemetry?: Record<string, never>;
279
+ policy?: Record<string, never>;
280
+ };
281
+ /** @description foreman: a run ended. */
282
+ RunCompleted: {
283
+ /** @constant */
284
+ specversion: "1.0";
285
+ /** @description Unique per source. A consumer that sees the same source and id twice handles the event once. */
286
+ id: components["schemas"]["Identifier"];
287
+ /** @description The producing module, for example "/osf/intake". */
288
+ source: string;
289
+ /** @constant */
290
+ type: "run.completed@v1";
291
+ /** Format: date-time */
292
+ time: string;
293
+ /** @description The ID of the thing the event is about: work item, gate, run, score or pull request. */
294
+ subject?: string;
295
+ /** @constant */
296
+ datacontenttype?: "application/json";
297
+ /** Format: uri */
298
+ dataschema?: string;
299
+ /** @description W3C trace context (CloudEvents distributed tracing extension). */
300
+ traceparent?: string;
301
+ data: components["schemas"]["RunCompletedData"];
302
+ };
303
+ RunCompletedData: {
304
+ runId: components["schemas"]["Identifier"];
305
+ workItemId: components["schemas"]["Identifier"];
306
+ stage?: components["schemas"]["Name"];
307
+ factoryVersion: components["schemas"]["CommitHash"];
308
+ outcome: components["schemas"]["RunOutcome"];
309
+ /** Format: date-time */
310
+ startedAt: string;
311
+ /** Format: date-time */
312
+ endedAt: string;
313
+ requester: components["schemas"]["Actor"];
314
+ source: components["schemas"]["Name"];
315
+ costUsd?: number;
316
+ traceId?: components["schemas"]["TraceId"];
317
+ pullRequests?: string[];
318
+ /** Format: uri */
319
+ evidenceBundle?: string;
320
+ };
321
+ /**
322
+ * @description How a run ended.
323
+ * @enum {string}
324
+ */
325
+ RunOutcome: "succeeded" | "failed" | "cancelled";
326
+ /**
327
+ * @description The attributes of one run: the OTel resource of every span of the run, and the attributes of its root
328
+ * `invoke_agent` span. The run record travels as OTLP; this schema fixes the attribute names and types.
329
+ * Realizes SCN-FACTORY-DATA-001 (record shape) and SCN-FACTORY-RUNTIME-008 (attribution).
330
+ */
331
+ RunRecord: {
332
+ resource: components["schemas"]["RunResource"];
333
+ span: components["schemas"]["RunSpan"];
334
+ };
335
+ /** @description Set at launch through OTEL_RESOURCE_ATTRIBUTES, so it is on every span of the run. */
336
+ RunResource: {
337
+ "service.name": string;
338
+ "osf.run.id": components["schemas"]["Identifier"];
339
+ "osf.work_item.id": components["schemas"]["Identifier"];
340
+ /** @description Commit of the factory definition the run used. */
341
+ "osf.factory.version": components["schemas"]["CommitHash"];
342
+ "osf.stage"?: components["schemas"]["Name"];
343
+ "osf.requester.kind": components["schemas"]["ActorKind"];
344
+ "osf.requester.id": string;
345
+ /** @description Where the run was started from, for example "slack", "github", "cli". */
346
+ "osf.source": components["schemas"]["Name"];
347
+ /** Format: uri */
348
+ "osf.origin.url"?: string;
349
+ /** @description Harness adapter, for example "claude-code". */
350
+ "osf.harness.name": components["schemas"]["Name"];
351
+ "osf.harness.version"?: string;
352
+ };
353
+ /** @description Attributes of the root span of the run. GenAI names where a convention exists. */
354
+ RunSpan: {
355
+ /** @constant */
356
+ "gen_ai.operation.name": "invoke_agent";
357
+ "gen_ai.provider.name"?: string;
358
+ "gen_ai.agent.name"?: string;
359
+ /** @description The harness session ID. */
360
+ "gen_ai.conversation.id"?: string;
361
+ "gen_ai.request.model"?: string;
362
+ "gen_ai.response.model"?: string;
363
+ "gen_ai.usage.input_tokens"?: number;
364
+ "gen_ai.usage.output_tokens"?: number;
365
+ "gen_ai.usage.cache_read.input_tokens"?: number;
366
+ "gen_ai.usage.cache_write.input_tokens"?: number;
367
+ "osf.outcome": components["schemas"]["RunOutcome"];
368
+ /** @description Cost of all model calls of the run, from the inference gateway. */
369
+ "osf.cost.usd"?: number;
370
+ /**
371
+ * Format: uri
372
+ * @description Full transcript, stored when the run ends.
373
+ */
374
+ "osf.transcript.uri"?: string;
375
+ /** @description Artifacts of the run: reports, screenshots, videos. */
376
+ "osf.artifact.uris"?: string[];
377
+ "osf.pull_request.urls"?: string[];
378
+ "osf.issue.urls"?: string[];
379
+ /** Format: uri */
380
+ "osf.evidence_bundle.uri"?: string;
381
+ /**
382
+ * Format: uri
383
+ * @description Agent Trace record with line attribution of the run's changes.
384
+ */
385
+ "osf.agent_trace.uri"?: string;
386
+ };
387
+ Sandbox: {
388
+ /** @description Sandbox template name, for example "node-24". */
389
+ template: string;
390
+ /** @description CPU cores. The smallest unit is 0.001 (one millicore). */
391
+ cpu?: number;
392
+ /** @description Kubernetes quantity, for example "8Gi". */
393
+ memory?: string;
394
+ /** @description Hostnames the sandbox may reach. Everything else is blocked. */
395
+ egress?: string[];
396
+ };
397
+ ScenarioEvidence: {
398
+ id: components["schemas"]["ScenarioId"];
399
+ result: components["schemas"]["ScenarioResult"];
400
+ /** @description Paths of the artifacts in this bundle that prove the scenario. */
401
+ artifacts: components["schemas"]["BundlePath"][];
402
+ summary?: string;
403
+ };
404
+ /** @description A scenario ID as checked by SPEC, for example SCN-PROOF-004. */
405
+ ScenarioId: string;
406
+ /** @enum {string} */
407
+ ScenarioResult: "passed" | "failed" | "skipped";
408
+ /**
409
+ * @description The result of one scorer on one run. The fields are a subset of Langfuse's create-score request
410
+ * (POST /api/public/scores), so a score can be sent to Langfuse as is. Factory fields live in `metadata`.
411
+ */
412
+ Score: {
413
+ id: components["schemas"]["Identifier"];
414
+ /** @description Scorer name, for example "redundant-tests". */
415
+ name: string;
416
+ /** @description A number for NUMERIC, 0 or 1 for BOOLEAN, a string for CATEGORICAL. */
417
+ value: number | string;
418
+ dataType: components["schemas"]["ScoreDataType"];
419
+ /** @description Trace of the run (the OTel trace ID). */
420
+ traceId: components["schemas"]["TraceId"];
421
+ /** @description Span the score is about, when it is not the whole run. */
422
+ observationId?: string;
423
+ sessionId?: string;
424
+ /** @description The reason. */
425
+ comment?: string;
426
+ /** Format: date-time */
427
+ timestamp: string;
428
+ /** @description API for scorers, ANNOTATION for scores a person reviews. */
429
+ source?: components["schemas"]["ScoreSource"];
430
+ configId?: string;
431
+ environment?: string;
432
+ metadata: components["schemas"]["ScoreMetadata"];
433
+ };
434
+ /** @enum {string} */
435
+ ScoreDataType: "NUMERIC" | "BOOLEAN" | "CATEGORICAL";
436
+ ScoreMetadata: {
437
+ "osf.run.id": components["schemas"]["Identifier"];
438
+ "osf.scorer.version"?: string;
439
+ /** @description Judge model of an LLM scorer. */
440
+ "osf.judge.model"?: string;
441
+ /** @description Cost of the judge call. */
442
+ "osf.judge.cost.usd"?: number;
443
+ };
444
+ /** @description scorekit: a scorer scored a run. */
445
+ ScoreRecorded: {
446
+ /** @constant */
447
+ specversion: "1.0";
448
+ /** @description Unique per source. A consumer that sees the same source and id twice handles the event once. */
449
+ id: components["schemas"]["Identifier"];
450
+ /** @description The producing module, for example "/osf/intake". */
451
+ source: string;
452
+ /** @constant */
453
+ type: "score.recorded@v1";
454
+ /** Format: date-time */
455
+ time: string;
456
+ /** @description The ID of the thing the event is about: work item, gate, run, score or pull request. */
457
+ subject?: string;
458
+ /** @constant */
459
+ datacontenttype?: "application/json";
460
+ /** Format: uri */
461
+ dataschema?: string;
462
+ /** @description W3C trace context (CloudEvents distributed tracing extension). */
463
+ traceparent?: string;
464
+ data: components["schemas"]["Score"];
465
+ };
466
+ /** @enum {string} */
467
+ ScoreSource: "API" | "ANNOTATION";
468
+ Scorer: {
469
+ /** @description Path to the Markdown judge prompt. */
470
+ judge: string;
471
+ /** @description Share of runs to score, 0 to 1. Default 1. */
472
+ sample?: number;
473
+ };
474
+ /** @description A reference to a secret, resolved at the egress proxy, never in the sandbox: "ref:<provider>/<name>". */
475
+ SecretReference: string;
476
+ Sha256: string;
477
+ Stage: {
478
+ stage: components["schemas"]["Name"];
479
+ /** @description Agent that runs the stage. Defaults to the agent with the stage's name. */
480
+ agent?: components["schemas"]["Name"];
481
+ /** @description The stage waits for a decision before it starts. */
482
+ gate?: components["schemas"]["Gate"];
483
+ /** @description Whether the stage must produce an evidence bundle. */
484
+ evidence?: components["schemas"]["EvidenceRequirement"];
485
+ };
486
+ /** @description An OpenTelemetry trace ID: 32 lowercase hex characters. */
487
+ TraceId: string;
488
+ /** @enum {string} */
489
+ VerificationResult: "passed" | "failed";
490
+ /** @description A semver range of an adapter, for example "^2". */
491
+ VersionRange: string;
492
+ /** @description One unit of requested work: the `data` of a `work.received@v1` CloudEvent. */
493
+ WorkItem: {
494
+ id: components["schemas"]["Identifier"];
495
+ type: components["schemas"]["WorkItemType"];
496
+ title: string;
497
+ description?: string;
498
+ /** @description The person or trigger that asked for the work. */
499
+ requester: components["schemas"]["Actor"];
500
+ /** @description Where the work came from. */
501
+ origin: components["schemas"]["Origin"];
502
+ /** @description Requests with the same key become one work item, for example "github:acme/web#42". */
503
+ dedupeKey: string;
504
+ /** @description Product surface the work is about, for example "checkout". */
505
+ surface?: string;
506
+ /** @description Set by triage. */
507
+ priority?: components["schemas"]["Priority"];
508
+ /** @description Related material: an error report, a change, a document. */
509
+ links?: components["schemas"]["Link"][];
510
+ };
511
+ /** @enum {string} */
512
+ WorkItemType: "idea" | "bug" | "task" | "automation";
513
+ /** @description intake: a work item was accepted. */
514
+ WorkReceived: {
515
+ /** @constant */
516
+ specversion: "1.0";
517
+ /** @description Unique per source. A consumer that sees the same source and id twice handles the event once. */
518
+ id: components["schemas"]["Identifier"];
519
+ /** @description The producing module, for example "/osf/intake". */
520
+ source: string;
521
+ /** @constant */
522
+ type: "work.received@v1";
523
+ /** Format: date-time */
524
+ time: string;
525
+ /** @description The ID of the thing the event is about: work item, gate, run, score or pull request. */
526
+ subject?: string;
527
+ /** @constant */
528
+ datacontenttype?: "application/json";
529
+ /** Format: uri */
530
+ dataschema?: string;
531
+ /** @description W3C trace context (CloudEvents distributed tracing extension). */
532
+ traceparent?: string;
533
+ data: components["schemas"]["WorkItem"];
534
+ };
535
+ };
536
+ responses: never;
537
+ parameters: never;
538
+ requestBodies: never;
539
+ headers: never;
540
+ pathItems: never;
541
+ };
542
+ export type $defs = Record<string, never>;
543
+ export type operations = Record<string, never>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { type Format } from "./formats.ts";
2
+ import type { components } from "./generated/types.ts";
3
+ export { diff as breakingChanges, type Schema } from "./breaking.ts";
4
+ export { FORMATS, type Format, formatNamed } from "./formats.ts";
5
+ export type { components } from "./generated/types.ts";
6
+ type Schemas = components["schemas"];
7
+ export type FactoryDefinition = Schemas["FactoryDefinition"];
8
+ export type WorkItem = Schemas["WorkItem"];
9
+ export type RunRecord = Schemas["RunRecord"];
10
+ export type EvidenceManifest = Schemas["EvidenceManifest"];
11
+ export type Score = Schemas["Score"];
12
+ export type WorkReceived = Schemas["WorkReceived"];
13
+ export type RunCompleted = Schemas["RunCompleted"];
14
+ export type GateRequested = Schemas["GateRequested"];
15
+ export type GateDecided = Schemas["GateDecided"];
16
+ export type ScoreRecorded = Schemas["ScoreRecorded"];
17
+ export type PrOpened = Schemas["PrOpened"];
18
+ /** One reason a document does not match its format. `path` is a JSON Pointer into the document. */
19
+ export interface Problem {
20
+ path: string;
21
+ message: string;
22
+ }
23
+ /** The JSON Schema of a format, as published in `<path>/<name>.schema.json`. */
24
+ export declare function schemaOf(format: Format | string): Record<string, unknown>;
25
+ /** Every problem of the document; empty when it matches the format. */
26
+ export declare function validate(format: Format | string, document: unknown): Problem[];
27
+ /** True when the document matches the format, with its type narrowed. */
28
+ export declare function isValid<T>(format: Format | string, document: unknown): document is T;
package/dist/index.js ADDED
@@ -0,0 +1,73 @@
1
+ // @opensoftware/contracts: the types of the shared formats and a validator for each. Modules import only from here.
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import Ajv2020, {} from "ajv/dist/2020.js";
6
+ import addFormats from "ajv-formats";
7
+ import { formatNamed } from "./formats.js";
8
+ export { diff as breakingChanges } from "./breaking.js";
9
+ export { FORMATS, formatNamed } from "./formats.js";
10
+ /** The package root: dist/ and src/ both sit one level below it, next to the schema folders. */
11
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
12
+ const validators = new Map();
13
+ /** The JSON Schema of a format, as published in `<path>/<name>.schema.json`. */
14
+ export function schemaOf(format) {
15
+ const found = typeof format === "string" ? formatNamed(format) : format;
16
+ if (!found)
17
+ throw new Error(`Unknown format: ${String(format)}`);
18
+ return JSON.parse(readFileSync(join(ROOT, found.path, `${found.name}.schema.json`), "utf8"));
19
+ }
20
+ /** Every problem of the document; empty when it matches the format. */
21
+ export function validate(format, document) {
22
+ const name = typeof format === "string" ? format : format.name;
23
+ let check = validators.get(name);
24
+ if (!check) {
25
+ // strictRequired is off: "exactly one of" rules use `not: { required: [...] }` on purpose.
26
+ const ajv = new Ajv2020.default({ allErrors: true, strict: true, strictRequired: false });
27
+ addFormats.default(ajv);
28
+ check = ajv.compile(schemaOf(format));
29
+ validators.set(name, check);
30
+ }
31
+ return check(document) ? [] : problems(check.errors ?? []);
32
+ }
33
+ /** True when the document matches the format, with its type narrowed. */
34
+ export function isValid(format, document) {
35
+ return validate(format, document).length === 0;
36
+ }
37
+ const ALTERNATIVES = new Set(["anyOf", "oneOf"]);
38
+ /** Ajv errors as one line per problem; the branches of a failed `anyOf` or `oneOf` collapse into one problem. */
39
+ function problems(errors) {
40
+ const alternatives = errors.filter((error) => ALTERNATIVES.has(error.keyword)).map((error) => error.instancePath);
41
+ const insideAlternative = (error) => !ALTERNATIVES.has(error.keyword) &&
42
+ /\/(anyOf|oneOf)\//.test(error.schemaPath) &&
43
+ alternatives.some((path) => error.instancePath === path || error.instancePath.startsWith(`${path}/`));
44
+ const seen = new Set();
45
+ const out = [];
46
+ for (const error of errors) {
47
+ if (insideAlternative(error))
48
+ continue;
49
+ const problem = describe(error);
50
+ const key = `${problem.path} ${problem.message}`;
51
+ if (seen.has(key))
52
+ continue;
53
+ seen.add(key);
54
+ out.push(problem);
55
+ }
56
+ return out;
57
+ }
58
+ function describe(error) {
59
+ const child = (name) => `${error.instancePath}/${name.replaceAll("~", "~0").replaceAll("/", "~1")}`;
60
+ switch (error.keyword) {
61
+ case "additionalProperties":
62
+ return { path: child(String(error.params.additionalProperty)), message: "unknown field" };
63
+ case "unevaluatedProperties":
64
+ return { path: child(String(error.params.unevaluatedProperty)), message: "unknown field" };
65
+ case "required":
66
+ return { path: child(String(error.params.missingProperty)), message: "missing required field" };
67
+ case "anyOf":
68
+ case "oneOf":
69
+ return { path: error.instancePath || "/", message: "must match exactly one of the allowed forms" };
70
+ default:
71
+ return { path: error.instancePath || "/", message: error.message ?? error.keyword };
72
+ }
73
+ }
@@ -0,0 +1,3 @@
1
+ export declare const CLIENTS: readonly ["typescript"];
2
+ export type Client = (typeof CLIENTS)[number];
3
+ export declare function roundtrip(_client: Client, dir: string): number;