qctl 0.2.1

Control in-repo YAML work queues
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
{
	"$defs": {
		"ArchiveOrder": {
			"description": "What order the archive is kept in.",
			"oneOf": [
				{
					"const": "newest_first",
					"description": "Most recently completed first, which is what `check` requires.",
					"type": "string"
				},
				{
					"const": "as_written",
					"description": "However the file has them. `fmt` will not reorder rows.",
					"type": "string"
				}
			]
		},
		"ArchivedTask": {
			"additionalProperties": false,
			"description": "A task that has left the queue, either shipped or deliberately dropped.",
			"properties": {
				"completed": {
					"description": "The moment it left the queue, as `YYYY-MM-DDThh:mm:ss`, in the zone\n[`Style::timezone`] declares.",
					"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
					"type": "string"
				},
				"disposition": {
					"anyOf": [
						{
							"$ref": "#/$defs/Disposition"
						},
						{
							"type": "null"
						}
					],
					"description": "Whether the work shipped or was dropped."
				},
				"evidence": {
					"description": "Where to look to see that it is true.",
					"items": {
						"minLength": 1,
						"type": "string"
					},
					"minItems": 1,
					"type": "array",
					"uniqueItems": true
				},
				"id": {
					"description": "The id it held on the queue, never reused after this.",
					"pattern": "^[A-Z][A-Z0-9]{1,7}-[0-9]{3,}$",
					"type": "string"
				},
				"links": {
					"description": "Anything outside this repository worth following.",
					"items": {
						"format": "uri",
						"type": "string"
					},
					"type": "array",
					"uniqueItems": true
				},
				"notes": {
					"description": "What a later reader will want to know and cannot reconstruct.",
					"minLength": 1,
					"type": [
						"string",
						"null"
					]
				},
				"outcome": {
					"description": "What became true.",
					"minLength": 1,
					"type": "string"
				},
				"patch": {
					"description": "The changeset that carried its release note.",
					"pattern": "^[a-z][a-z0-9-]*$",
					"type": [
						"string",
						"null"
					]
				},
				"plan": {
					"description": "The plan it was worked from, if it had one.",
					"pattern": "^(?!/)(?!.*\\.\\.).+\\.md$",
					"type": [
						"string",
						"null"
					]
				},
				"scope": {
					"description": "Where the work landed.",
					"minLength": 1,
					"type": "string"
				},
				"title": {
					"description": "What the task was, in one line.",
					"minLength": 1,
					"type": "string"
				}
			},
			"required": [
				"id",
				"title",
				"scope",
				"completed",
				"outcome",
				"evidence"
			],
			"type": "object"
		},
		"Disposition": {
			"description": "How a task left the queue.",
			"oneOf": [
				{
					"const": "completed",
					"description": "The outcome was reached.",
					"type": "string"
				},
				{
					"const": "dropped",
					"description": "The task was abandoned on purpose, and the row says why.",
					"type": "string"
				}
			]
		},
		"HorizonTask": {
			"additionalProperties": false,
			"description": "Work that is mapped but has no place on the queue yet.",
			"properties": {
				"id": {
					"description": "Its id, drawn from the same sequence as the queue's.",
					"pattern": "^[A-Z][A-Z0-9]{1,7}-[0-9]{3,}$",
					"type": "string"
				},
				"kind": {
					"$ref": "#/$defs/Kind",
					"description": "Why it is not on the queue."
				},
				"links": {
					"description": "Anything outside this repository worth following.",
					"items": {
						"format": "uri",
						"type": "string"
					},
					"type": "array",
					"uniqueItems": true
				},
				"notes": {
					"description": "Context a reader needs and the row cannot carry in its other fields.",
					"minLength": 1,
					"type": [
						"string",
						"null"
					]
				},
				"open": {
					"description": "The missing start condition, or the question that keeps it off the\nqueue.",
					"minLength": 1,
					"type": "string"
				},
				"outcome": {
					"description": "What would be true once it is done.",
					"minLength": 1,
					"type": "string"
				},
				"patch": {
					"description": "The changeset it would carry its release note in.",
					"pattern": "^[a-z][a-z0-9-]*$",
					"type": [
						"string",
						"null"
					]
				},
				"plan": {
					"description": "A plan document in this repository, when it already has one.",
					"pattern": "^(?!/)(?!.*\\.\\.).+\\.md$",
					"type": [
						"string",
						"null"
					]
				},
				"scope": {
					"description": "Where the work would land.",
					"minLength": 1,
					"type": "string"
				},
				"title": {
					"description": "What it is, in one line.",
					"minLength": 1,
					"type": "string"
				}
			},
			"required": [
				"id",
				"title",
				"scope",
				"outcome",
				"kind",
				"open"
			],
			"type": "object"
		},
		"Kind": {
			"description": "Why a task sits on the horizon instead of the queue.",
			"oneOf": [
				{
					"const": "research",
					"description": "Facts are missing.",
					"type": "string"
				},
				{
					"const": "evaluation",
					"description": "The options are known, but nothing says when to choose.",
					"type": "string"
				},
				{
					"const": "deferred",
					"description": "The start condition is explicit and not met.",
					"type": "string"
				}
			]
		},
		"QueuedTask": {
			"additionalProperties": false,
			"description": "A task on the queue.",
			"properties": {
				"acceptance": {
					"description": "What has to be demonstrably true to close it.",
					"items": {
						"minLength": 1,
						"type": "string"
					},
					"minItems": 1,
					"type": "array",
					"uniqueItems": true
				},
				"blocked_by": {
					"description": "Ids that must ship first. Each has to sit earlier in this queue.",
					"items": {
						"pattern": "^[A-Z][A-Z0-9]{1,7}-[0-9]{3,}$",
						"type": "string"
					},
					"type": "array",
					"uniqueItems": true
				},
				"id": {
					"description": "This task's id, unique for the life of the repository.",
					"pattern": "^[A-Z][A-Z0-9]{1,7}-[0-9]{3,}$",
					"type": "string"
				},
				"links": {
					"description": "Anything outside this repository worth following.",
					"items": {
						"format": "uri",
						"type": "string"
					},
					"type": "array",
					"uniqueItems": true
				},
				"notes": {
					"description": "Context a reader needs and the row cannot carry in its other fields.",
					"minLength": 1,
					"type": [
						"string",
						"null"
					]
				},
				"outcome": {
					"description": "What is true once this is done.",
					"minLength": 1,
					"type": "string"
				},
				"patch": {
					"description": "The changeset that will carry this task's release note.",
					"pattern": "^[a-z][a-z0-9-]*$",
					"type": [
						"string",
						"null"
					]
				},
				"plan": {
					"description": "A plan document in this repository, when the task needs one.",
					"pattern": "^(?!/)(?!.*\\.\\.).+\\.md$",
					"type": [
						"string",
						"null"
					]
				},
				"scope": {
					"description": "Where the work lands.",
					"minLength": 1,
					"type": "string"
				},
				"title": {
					"description": "What the task is, in one line.",
					"minLength": 1,
					"type": "string"
				}
			},
			"required": [
				"id",
				"title",
				"scope",
				"outcome",
				"blocked_by",
				"acceptance"
			],
			"type": "object"
		},
		"Section": {
			"description": "One of a ledger's three lists.",
			"enum": [
				"queue",
				"archive",
				"horizon"
			],
			"type": "string"
		},
		"Style": {
			"additionalProperties": false,
			"description": "How a ledger is written, declared by the ledger itself.\n\nEvery option has a default that is what qctl already wrote, so a file with no\n`style` block keeps the shape it has. `qctl fmt` applies these; a verb writes\nany new text in them and, unless [`Style::normalize_on_write`] says otherwise,\nleaves the rest of the file alone.\n\nThis is meant to grow. A new option is a new field with a default equal to\ntoday's behaviour, which is a minor change rather than a migration.",
			"properties": {
				"archive_order": {
					"$ref": "#/$defs/ArchiveOrder",
					"default": "newest_first",
					"description": "Whether `fmt` sorts the archive newest-first or leaves it as written."
				},
				"indent": {
					"default": 2,
					"description": "How far a row is indented under its list's key.",
					"format": "uint8",
					"maximum": 8,
					"minimum": 1,
					"type": "integer"
				},
				"normalize_on_write": {
					"default": false,
					"description": "Whether a verb normalizes the whole file on the way out.\n\nFalse, and a verb rewrites only the lines it changes — so a diff shows\nthe work and nothing else. True, and every verb leaves the file fully\nnormalized, at the price of touching lines nobody asked about.",
					"type": "boolean"
				},
				"section_order": {
					"default": [
						"queue",
						"archive",
						"horizon"
					],
					"description": "The order the lists appear in the file. All three, once each, in whatever\norder reads best here.",
					"items": {
						"$ref": "#/$defs/Section"
					},
					"maxItems": 3,
					"minItems": 3,
					"type": "array",
					"uniqueItems": true
				},
				"timezone": {
					"default": "+00:00",
					"description": "The fixed offset `completed` is written in. Stamps carry no offset of\ntheir own — this is where they say what zone they are.\n\nChanging it does not rewrite the stamps already written: nothing records\nwhich zone an existing stamp was taken in, so `fmt` cannot know, and\nguessing would move a moment. Change it deliberately, or not at all.",
					"pattern": "^[+-][0-9]{2}:[0-9]{2}$",
					"type": "string"
				}
			},
			"type": "object"
		}
	},
	"$id": "https://raw.githubusercontent.com/victor-software-house/qctl/main/schema/tasks.schema.json",
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"additionalProperties": false,
	"description": "One repository's work queue.",
	"properties": {
		"active": {
			"description": "The one task currently being executed, or null when work is\nintentionally paused.",
			"pattern": "^[A-Z][A-Z0-9]{1,7}-[0-9]{3,}$",
			"type": [
				"string",
				"null"
			]
		},
		"archive": {
			"description": "Completed or deliberately dropped work, newest first. Archived ids are\nnever reused.",
			"items": {
				"$ref": "#/$defs/ArchivedTask"
			},
			"type": "array"
		},
		"horizon": {
			"description": "Mapped work that is not on the short-term queue: research, evaluations,\nor deferred items without a start condition. File order is not\npriority, and `active` must never name a horizon id.",
			"items": {
				"$ref": "#/$defs/HorizonTask"
			},
			"type": "array"
		},
		"prefix": {
			"description": "Stable id prefix for this repository. Never encodes priority.",
			"pattern": "^[A-Z][A-Z0-9]{1,7}$",
			"type": "string"
		},
		"queue": {
			"description": "Pending work in priority order. The order is the priority.",
			"items": {
				"$ref": "#/$defs/QueuedTask"
			},
			"type": "array"
		},
		"schema_version": {
			"const": 3,
			"description": "Ledger schema version, the one this qctl writes and accepts. Increment\nonly with a coordinated schema and ledger migration.",
			"format": "uint32",
			"minimum": 0,
			"type": "integer"
		},
		"style": {
			"$ref": "#/$defs/Style",
			"default": {
				"archive_order": "newest_first",
				"indent": 2,
				"normalize_on_write": false,
				"section_order": [
					"queue",
					"archive",
					"horizon"
				],
				"timezone": "+00:00"
			},
			"description": "How this ledger is written. A file that says nothing here gets the\ndefaults, which are the shape qctl already wrote."
		}
	},
	"required": [
		"schema_version",
		"prefix",
		"active",
		"queue",
		"archive"
	],
	"title": "qctl work queue",
	"type": "object"
}