Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions planfile/schemas/ticket-status.v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"schema": "planfile.ticket-status/v1",
"description": "Every planfile TicketStatus value and its class, for consumers that classify queues without importing planfile (e.g. monag resume, fleet admission). A status missing here is a contract defect, not an unknown value to tolerate.",
"classes": {
"ready": "Pending work that may be picked up now.",
"claimed": "Pending work held by an executor.",
"awaiting_review": "Pending work waiting for review.",
"stalled": "Pending work that needs triage before anyone may pick it up again; never ready.",
"terminal_success": "Finished with the intended outcome.",
"terminal_not_done": "Finished without delivering the outcome."
},
"statuses": {
"open": {"class": "ready", "pending": true},
"in_progress": {"class": "claimed", "pending": true},
"review": {"class": "awaiting_review", "pending": true},
"blocked": {"class": "stalled", "pending": true},
"failed": {"class": "stalled", "pending": true, "note": "Execution attempts exhausted; it may be reopened."},
"done": {"class": "terminal_success", "pending": false},
"canceled": {"class": "terminal_not_done", "pending": false}
}
}
1 change: 1 addition & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,5 @@ This file indexes governance tickets without taking ownership of
| **ticket-210** | [`README.md`](./ticket-210/README.md) | - | - | - | - | - |
| **ticket-211** | [`README.md`](./ticket-211/README.md) | - | - | - | - | - |
| **ticket-212** | [`README.md`](./ticket-212/README.md) | - | - | - | - | - |
| **ticket-213** | [`README.md`](./ticket-213/README.md) | - | - | - | - | - |
<!-- AUTO:TICKET_INDEX:END -->
34 changes: 34 additions & 0 deletions project/ticket-214/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Ticket 214: Publish a machine-readable ticket status contract

- **ID**: ticket-214
- **Owner**: claude
- **Status**: IN_PROGRESS
- **Workflow state**: EDIT
- **Created**: 2026-10-15

## Goal and scope

Queue consumers outside planfile keep private copies of the status
vocabulary. On 2026-10-15 `monag resume` did not know `failed`, counted it as
an unknown status, and the fleet admission gate then refused willman project
intake for every repository holding one failed ticket (352 local
repositories; fixed in semcod/monag#141).

planfile now publishes `planfile/schemas/ticket-status.v1.json`
(`planfile.ticket-status/v1`): every `TicketStatus` value with a class and a
`pending` flag. `blocked` and `failed` are `stalled` (pending, never ready),
matching their existing behaviour (`failed` = execution attempts exhausted,
reopenable). `schemas/*.json` is already package data, so the contract ships
with every installation. A test fails if the enum and the contract diverge.

## Acceptance criteria

- [x] AC-01: `tests/test_ticket_status_contract.py`: the contract lists exactly
the `TicketStatus` values, each with a declared class and a consistent
`pending` flag; only `open` is ready; `blocked`/`failed` are stalled.
- [x] AC-02: full test suite: 1244 passed, 6 skipped.
- [x] AC-03: `project/governance-check.sh` passes.

## Tracking boundary

Adoption by consumers (monag) is a separate ticket in their repositories.
92 changes: 92 additions & 0 deletions project/ticket-214/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-214",
"summary": "Publish a machine-readable ticket status contract",
"workstream": "application",
"classification": {
"kind": "SERVICE",
"priority": "P2",
"origin": "requested"
},
"allowedPaths": [
"project/ticket-214/**",
"TODO.md",
"project/TICKETS.md",
"planfile/schemas/ticket-status.v1.json",
"tests/test_ticket_status_contract.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [
"python"
],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "a52495a84cd6ba90aca1ee012462731b5ff1521c",
"targetBranch": "main",
"outcome": "planfile ships planfile/schemas/ticket-status.v1.json: every TicketStatus value with a class (ready, claimed, awaiting_review, stalled, terminal_success, terminal_not_done) and a pending flag, guarded by a test against the enum, so queue consumers (monag resume, fleet admission) classify statuses from one source instead of private OPEN/CLOSED sets.",
"nonGoals": [
"No change to TicketStatus or any status behaviour.",
"No consumer change in this repository (monag adopts it separately)."
],
"complexity": "S",
"estimatedMinutes": 20,
"budgets": {
"maxImplementationFiles": 2,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 1,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Publish a JSON contract next to the existing packaged schemas (package-data schemas/*.json already ships it) and test it against TicketStatus.",
"components": [
{
"name": "ticket-status-contract",
"paths": [
"planfile/schemas/ticket-status.v1.json",
"tests/test_ticket_status_contract.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [
"New packaged read-only contract planfile/schemas/ticket-status.v1.json (planfile.ticket-status/v1)"
],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "git revert removes the contract file and its test."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python -m pytest -q tests/test_ticket_status_contract.py"
],
"evidence": "project/ticket-214/README.md"
},
{
"criterion": "AC-02",
"commands": [
"python -m pytest -q tests"
],
"evidence": "project/ticket-214/README.md"
},
{
"criterion": "AC-03",
"commands": [
"bash project/governance-check.sh"
],
"evidence": "project/ticket-214/README.md"
}
]
}
}
28 changes: 28 additions & 0 deletions tests/test_ticket_status_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""The published status contract covers exactly the TicketStatus enum (ticket-214)."""
import json
from importlib import resources

from planfile.core.models.base import TicketStatus


def contract():
return json.loads(resources.files("planfile").joinpath("schemas/ticket-status.v1.json").read_text())


def test_contract_lists_every_ticket_status_exactly_once():
assert set(contract()["statuses"]) == {status.value for status in TicketStatus}


def test_every_status_has_a_declared_class_and_pending_flag():
data = contract()
assert data["schema"] == "planfile.ticket-status/v1"
for name, entry in data["statuses"].items():
assert entry["class"] in data["classes"], name
assert isinstance(entry["pending"], bool), name
assert entry["pending"] == (not entry["class"].startswith("terminal_")), name


def test_only_open_is_ready_and_stalled_work_is_pending():
statuses = contract()["statuses"]
assert [name for name, entry in statuses.items() if entry["class"] == "ready"] == ["open"]
assert {name for name, entry in statuses.items() if entry["class"] == "stalled"} == {"blocked", "failed"}
Loading