Skip to content

Commit 7b6aadc

Browse files
feat(contract): add work_item_ref shared schema (#1590) (#1593)
Phase 2.1 of Epic #1565 (work-source dispatch). Adds a draft-07 JSON schema at internal/contract/schemas/shared/work_item_ref.json so future WorkSourceService, /work board, and dispatch wiring share a validated shape for forge issues/PRs, scheduled jobs, and manual triggers. Schema details: - $id wave://shared/work_item_ref, title WorkItemRef. - source enum (github, gitea, gitlab, bitbucket, schedule, manual) discriminates forge vs non-forge entries. - Unconditional required: source, url, title, state, created_at. - forge_host, owner, repo are required only for forge sources via allOf if/then; number stays optional even for forge sources because some forges expose numberless work items. - additionalProperties:false for parity with issue_ref / pr_ref. Also: - registry_test.go canonical list includes work_item_ref; sorted-floor bumped 7 -> 8. - New work_item_ref_test.go covers six positive fixtures (one per source) and five negatives (missing forge_host, unknown source, extra property, invalid state, malformed created_at). - ADR-010 updated: schema count 8 -> 9 and a Schema additions footer table. - docs/scope/onboarding-as-session-plan.md: Phase 2.1 row ticked, inline draft snippet replaced with the canonical-shape diff (forge -> source, repo string -> forge_host/owner/repo, kind/id -> number + state). .agents/contracts/ deliberately not mirrored: that registry holds step-output contracts validated by sync_test.go, not pipeline-I/O typed schemas (different system).
1 parent bf39994 commit 7b6aadc

8 files changed

Lines changed: 537 additions & 20 deletions

File tree

docs/adr/010-pipeline-io-protocol.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Accepted (Phase 1 — load-time validation live; pipeline migration ongoing)
99
## Implementation Status
1010

1111
Landed:
12-
- Eight canonical schemas in `internal/contract/schemas/shared/*.json` (`issue_ref`, `pr_ref`, `scope_result`, `plan_ref`, `findings_report`, `workspace_ref`, `spec_ref`, `branch_ref`).
12+
- Nine canonical schemas in `internal/contract/schemas/shared/*.json` (`issue_ref`, `pr_ref`, `scope_result`, `plan_ref`, `findings_report`, `workspace_ref`, `spec_ref`, `branch_ref`, `work_item_ref`).
1313
- Registry: `internal/contract/schemas/registry.go` (+ test).
1414
- Typed I/O fields: `InputConfig.Type` (`internal/pipeline/types.go:138`) and `PipelineOutput.Type` (`types.go:725`); both expose `EffectiveType()` defaulting to `"string"`.
1515
- Load-time validation: `ValidatePipelineIOTypes` and `TypedWiringCheck` in `internal/pipeline/iotypes.go` (~216 LOC), wired into the loader at `dag.go:42` and `dag.go:52`.
@@ -63,7 +63,7 @@ Introduce a **typed I/O protocol** with four pieces:
6363
`internal/contract/schemas/shared/*.json`, embedded into the Wave
6464
binary via `go:embed`. Each file's basename is the type name
6565
(`issue_ref`, `pr_ref`, `branch_ref`, `spec_ref`, `findings_report`,
66-
`plan_ref`, `workspace_ref`, `scope_result`). A pipeline's type
66+
`plan_ref`, `workspace_ref`, `scope_result`, `work_item_ref`). A pipeline's type
6767
annotation must resolve against this registry or the sentinel
6868
`string`.
6969

@@ -193,7 +193,7 @@ on input/output, load-time validation.
193193
## Implementation Notes
194194

195195
Files created:
196-
- `internal/contract/schemas/shared/*.json` (8 canonical schemas)
196+
- `internal/contract/schemas/shared/*.json` (9 canonical schemas)
197197
- `internal/contract/schemas/shared/registry.go` (+ test)
198198
- `internal/pipeline/iotypes.go` (+ test)
199199
- `docs/adr/010-pipeline-io-protocol.md` (this file)
@@ -255,3 +255,9 @@ by design — they exercise specific executor paths and need free shape.
255255
decide whether to wire shared types into the runtime contract path.
256256
- No deep `field`-extraction on `pipeline_outputs` with type
257257
narrowing. The existing `field:` hint stays as-is.
258+
259+
## Schema additions
260+
261+
| Date | Type | Issue / PR | Notes |
262+
|---|---|---|---|
263+
| 2026-04-30 | `work_item_ref` | #1590 (Epic #1565 Phase 2.1) | Canonical work-source dispatch shape; `source` enum (`github`, `gitea`, `gitlab`, `bitbucket`, `schedule`, `manual`) discriminates forge vs non-forge entries. Forge sources require `forge_host`, `owner`, `repo` via `if/then`; `number` stays optional even for forge sources to accommodate numberless work items. Not mirrored to `.agents/contracts/` — that registry holds step-output contracts, not shared pipeline-I/O types. |

docs/scope/onboarding-as-session-plan.md

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -389,20 +389,12 @@ See PRE-5. Five new tables; ~10 new `StateStore` methods. Zero existing column c
389389

390390
New canonical schema (per ADR-010): `internal/contract/schemas/shared/work_item_ref.json` — generalizes `issue_ref` / `pr_ref` for forge-agnostic work items.
391391

392-
```json
393-
{
394-
"type": "object",
395-
"required": ["forge", "repo", "kind", "id"],
396-
"properties": {
397-
"forge": { "enum": ["github", "gitea", "gitlab", "bitbucket"] },
398-
"repo": { "type": "string" }, // owner/name
399-
"kind": { "enum": ["issue", "pr", "task"] },
400-
"id": { "type": "string" },
401-
"title": { "type": "string" },
402-
"url": { "type": "string", "format": "uri" }
403-
}
404-
}
405-
```
392+
The shipped shape is canonicalized in [`internal/contract/schemas/shared/work_item_ref.json`](../../internal/contract/schemas/shared/work_item_ref.json). It diverged from the earlier draft below:
393+
394+
- `source` (not `forge`) discriminates entries — enum extended with `schedule` and `manual` so non-forge work items fit cleanly.
395+
- `forge_host`, `owner`, `repo` replace the combined `repo: "owner/name"` string and are conditionally required (`if/then` on `source`) only for forge entries.
396+
- `number` (integer) supersedes the polymorphic `id` and `kind` fields. Issue-vs-PR distinction lives in the `state` enum (`open | closed | merged`) and the URL itself, not a separate type tag.
397+
- `state` and `created_at` are required; `labels` is an optional array of strings.
406398

407399
---
408400

@@ -432,7 +424,7 @@ New canonical schema (per ADR-010): `internal/contract/schemas/shared/work_item_
432424

433425
| # | Title | Files |
434426
|---|---|---|
435-
| 2.1 | `work_item_ref` shared schema | `internal/contract/schemas/shared/work_item_ref.json` + registry |
427+
| 2.1 | `work_item_ref` shared schema | `internal/contract/schemas/shared/work_item_ref.json` + registry |
436428
| 2.2 | WorkSourceService + bindings | `internal/service/worksource.go`, `internal/worksource/`, table CRUD |
437429
| 2.3 | Webui `/work` board + detail | new templates, handlers; replaces dashboard as default landing |
438430
| 2.4 | "Run on this issue" button | binding lookup → `ExecutorService.Run` |

internal/contract/schemas/shared/registry_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestRegistryContainsCanonicalTypes(t *testing.T) {
1414
"findings_report",
1515
"plan_ref",
1616
"workspace_ref",
17+
"work_item_ref",
1718
}
1819
for _, name := range expected {
1920
data, ok := Lookup(name)
@@ -63,8 +64,8 @@ func TestUnknownType(t *testing.T) {
6364

6465
func TestNamesSorted(t *testing.T) {
6566
names := Names()
66-
if len(names) < 7 {
67-
t.Errorf("Names() returned %d entries, want at least 7", len(names))
67+
if len(names) < 8 {
68+
t.Errorf("Names() returned %d entries, want at least 8", len(names))
6869
}
6970
for i := 1; i < len(names); i++ {
7071
if names[i-1] >= names[i] {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "wave://shared/work_item_ref",
4+
"title": "WorkItemRef",
5+
"description": "Canonical reference to a unit of work emitted by a work-source (forge issue/PR, scheduled job, manual trigger) and consumed by pipelines. Forge-specific fields (forge_host, owner, repo, number) are required only when source identifies a git forge; schedule/manual sources omit them.",
6+
"type": "object",
7+
"required": ["source", "url", "title", "state", "created_at"],
8+
"properties": {
9+
"source": {
10+
"type": "string",
11+
"enum": ["github", "gitea", "gitlab", "bitbucket", "schedule", "manual"],
12+
"description": "Origin of the work item. Forge values (github, gitea, gitlab, bitbucket) imply forge_host, owner, repo are required."
13+
},
14+
"forge_host": {
15+
"type": "string",
16+
"minLength": 1,
17+
"description": "Hostname of the forge instance (e.g. github.com, codeberg.org, gitlab.example.com). Required for forge sources."
18+
},
19+
"owner": {
20+
"type": "string",
21+
"minLength": 1,
22+
"description": "Owner (user or organization). Required for forge sources."
23+
},
24+
"repo": {
25+
"type": "string",
26+
"minLength": 1,
27+
"description": "Repository name. Required for forge sources."
28+
},
29+
"number": {
30+
"type": "integer",
31+
"minimum": 1,
32+
"description": "Issue or PR number. Optional even for forge sources because some forges expose numberless work items (e.g. Gitea task lists)."
33+
},
34+
"url": {
35+
"type": "string",
36+
"format": "uri",
37+
"description": "Canonical URL identifying the work item. Used as the primary key in the worksource_binding table."
38+
},
39+
"title": {
40+
"type": "string",
41+
"minLength": 1,
42+
"description": "Human-readable title of the work item."
43+
},
44+
"labels": {
45+
"type": "array",
46+
"description": "Labels or tags attached to the work item. May be empty; absent for sources that have no label concept.",
47+
"items": {
48+
"type": "string",
49+
"minLength": 1
50+
}
51+
},
52+
"state": {
53+
"type": "string",
54+
"enum": ["open", "closed", "merged"],
55+
"description": "Lifecycle state. 'merged' applies only to PR-like work items; consumers may filter per source."
56+
},
57+
"created_at": {
58+
"type": "string",
59+
"format": "date-time",
60+
"description": "RFC 3339 timestamp marking when the work item was created."
61+
}
62+
},
63+
"allOf": [
64+
{
65+
"if": {
66+
"properties": {
67+
"source": {
68+
"enum": ["github", "gitea", "gitlab", "bitbucket"]
69+
}
70+
},
71+
"required": ["source"]
72+
},
73+
"then": {
74+
"required": ["forge_host", "owner", "repo"]
75+
}
76+
}
77+
],
78+
"additionalProperties": false
79+
}

0 commit comments

Comments
 (0)