@kitsy/coop-core 0.0.1
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/LICENSE +22 -0
- package/README.md +7 -0
- package/dist/index.cjs +2097 -0
- package/dist/index.d.cts +670 -0
- package/dist/index.d.ts +670 -0
- package/dist/index.js +1998 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2097 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ArtifactType: () => ArtifactType,
|
|
34
|
+
CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
|
|
35
|
+
DeliveryStatus: () => DeliveryStatus,
|
|
36
|
+
ExecutorType: () => ExecutorType,
|
|
37
|
+
ITEM_STATUSES: () => ITEM_STATUSES,
|
|
38
|
+
ITEM_TYPES: () => ITEM_TYPES,
|
|
39
|
+
IdeaStatus: () => IdeaStatus,
|
|
40
|
+
RiskLevel: () => RiskLevel,
|
|
41
|
+
RunbookAction: () => RunbookAction,
|
|
42
|
+
TaskComplexity: () => TaskComplexity,
|
|
43
|
+
TaskDeterminism: () => TaskDeterminism,
|
|
44
|
+
TaskPriority: () => TaskPriority,
|
|
45
|
+
TaskStatus: () => TaskStatus,
|
|
46
|
+
TaskType: () => TaskType,
|
|
47
|
+
VALID_TASK_TRANSITIONS: () => VALID_TASK_TRANSITIONS,
|
|
48
|
+
VALID_TRANSITIONS: () => VALID_TRANSITIONS,
|
|
49
|
+
build_graph: () => build_graph,
|
|
50
|
+
check_blocked: () => check_blocked,
|
|
51
|
+
check_unblocked: () => check_unblocked,
|
|
52
|
+
completeItem: () => completeItem,
|
|
53
|
+
compute_all_readiness: () => compute_all_readiness,
|
|
54
|
+
compute_readiness: () => compute_readiness,
|
|
55
|
+
compute_readiness_with_corrections: () => compute_readiness_with_corrections,
|
|
56
|
+
createItem: () => createItem,
|
|
57
|
+
deleteItem: () => deleteItem,
|
|
58
|
+
detect_cycle: () => detect_cycle,
|
|
59
|
+
ensureCoopLayout: () => ensureCoopLayout,
|
|
60
|
+
extract_subgraph: () => extract_subgraph,
|
|
61
|
+
findRepoRoot: () => findRepoRoot,
|
|
62
|
+
find_external_dependencies: () => find_external_dependencies,
|
|
63
|
+
getItemById: () => getItemById,
|
|
64
|
+
loadState: () => loadState,
|
|
65
|
+
load_graph: () => load_graph,
|
|
66
|
+
parseDeliveryContent: () => parseDeliveryContent,
|
|
67
|
+
parseDeliveryFile: () => parseDeliveryFile,
|
|
68
|
+
parseFrontmatterContent: () => parseFrontmatterContent,
|
|
69
|
+
parseFrontmatterFile: () => parseFrontmatterFile,
|
|
70
|
+
parseIdeaContent: () => parseIdeaContent,
|
|
71
|
+
parseIdeaFile: () => parseIdeaFile,
|
|
72
|
+
parseTaskContent: () => parseTaskContent,
|
|
73
|
+
parseTaskFile: () => parseTaskFile,
|
|
74
|
+
parseYamlContent: () => parseYamlContent,
|
|
75
|
+
parseYamlFile: () => parseYamlFile,
|
|
76
|
+
partition_by_readiness: () => partition_by_readiness,
|
|
77
|
+
queryItems: () => queryItems,
|
|
78
|
+
read_schema_version: () => read_schema_version,
|
|
79
|
+
renderAgentPrompt: () => renderAgentPrompt,
|
|
80
|
+
stringifyFrontmatter: () => stringifyFrontmatter,
|
|
81
|
+
topological_sort: () => topological_sort,
|
|
82
|
+
transition: () => transition,
|
|
83
|
+
transitive_dependencies: () => transitive_dependencies,
|
|
84
|
+
transitive_dependents: () => transitive_dependents,
|
|
85
|
+
updateItem: () => updateItem,
|
|
86
|
+
validate: () => validate,
|
|
87
|
+
validateReferential: () => validateReferential,
|
|
88
|
+
validateRepo: () => validateRepo,
|
|
89
|
+
validateSemantic: () => validateSemantic,
|
|
90
|
+
validateStructural: () => validateStructural,
|
|
91
|
+
validateTransition: () => validateTransition,
|
|
92
|
+
validate_graph: () => validate_graph,
|
|
93
|
+
validate_transition: () => validate_transition,
|
|
94
|
+
writeTask: () => writeTask,
|
|
95
|
+
write_schema_version: () => write_schema_version
|
|
96
|
+
});
|
|
97
|
+
module.exports = __toCommonJS(index_exports);
|
|
98
|
+
|
|
99
|
+
// src/graph/dag.ts
|
|
100
|
+
function existingDeps(graph, nodeId) {
|
|
101
|
+
const deps = graph.forward.get(nodeId) ?? /* @__PURE__ */ new Set();
|
|
102
|
+
return Array.from(deps).filter((depId) => graph.nodes.has(depId));
|
|
103
|
+
}
|
|
104
|
+
function detect_cycle(graph) {
|
|
105
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
106
|
+
const visited = /* @__PURE__ */ new Set();
|
|
107
|
+
const stack = [];
|
|
108
|
+
const dfs = (nodeId) => {
|
|
109
|
+
visiting.add(nodeId);
|
|
110
|
+
stack.push(nodeId);
|
|
111
|
+
for (const depId of existingDeps(graph, nodeId)) {
|
|
112
|
+
if (!visited.has(depId)) {
|
|
113
|
+
if (visiting.has(depId)) {
|
|
114
|
+
const start = stack.lastIndexOf(depId);
|
|
115
|
+
if (start >= 0) {
|
|
116
|
+
return [...stack.slice(start), depId];
|
|
117
|
+
}
|
|
118
|
+
return [depId, depId];
|
|
119
|
+
}
|
|
120
|
+
const cycle = dfs(depId);
|
|
121
|
+
if (cycle) return cycle;
|
|
122
|
+
} else if (visiting.has(depId)) {
|
|
123
|
+
const start = stack.lastIndexOf(depId);
|
|
124
|
+
if (start >= 0) {
|
|
125
|
+
return [...stack.slice(start), depId];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
stack.pop();
|
|
130
|
+
visiting.delete(nodeId);
|
|
131
|
+
visited.add(nodeId);
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
const nodeIds = Array.from(graph.nodes.keys()).sort((a, b) => a.localeCompare(b));
|
|
135
|
+
for (const nodeId of nodeIds) {
|
|
136
|
+
if (visited.has(nodeId)) continue;
|
|
137
|
+
const cycle = dfs(nodeId);
|
|
138
|
+
if (cycle) return cycle;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function topological_sort(graph) {
|
|
143
|
+
const indegree = /* @__PURE__ */ new Map();
|
|
144
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const nodeId of graph.nodes.keys()) {
|
|
146
|
+
indegree.set(nodeId, 0);
|
|
147
|
+
dependents.set(nodeId, /* @__PURE__ */ new Set());
|
|
148
|
+
}
|
|
149
|
+
for (const [nodeId, deps] of graph.forward.entries()) {
|
|
150
|
+
for (const depId of deps) {
|
|
151
|
+
if (!graph.nodes.has(depId)) continue;
|
|
152
|
+
indegree.set(nodeId, (indegree.get(nodeId) ?? 0) + 1);
|
|
153
|
+
dependents.get(depId)?.add(nodeId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const queue = Array.from(indegree.entries()).filter(([, degree]) => degree === 0).map(([nodeId]) => nodeId).sort((a, b) => a.localeCompare(b));
|
|
157
|
+
const result = [];
|
|
158
|
+
while (queue.length > 0) {
|
|
159
|
+
const nodeId = queue.shift();
|
|
160
|
+
if (!nodeId) continue;
|
|
161
|
+
result.push(nodeId);
|
|
162
|
+
const blocked = Array.from(dependents.get(nodeId) ?? []);
|
|
163
|
+
for (const dependentId of blocked) {
|
|
164
|
+
const next = (indegree.get(dependentId) ?? 0) - 1;
|
|
165
|
+
indegree.set(dependentId, next);
|
|
166
|
+
if (next === 0) {
|
|
167
|
+
queue.push(dependentId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
queue.sort((a, b) => a.localeCompare(b));
|
|
171
|
+
}
|
|
172
|
+
if (result.length !== graph.nodes.size) {
|
|
173
|
+
const cycle = detect_cycle(graph);
|
|
174
|
+
const cycleText = cycle ? cycle.join(" -> ") : "unknown cycle";
|
|
175
|
+
throw new Error(`Dependency cycle detected: ${cycleText}`);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
function transitive_dependencies(graph, taskId) {
|
|
180
|
+
const result = /* @__PURE__ */ new Set();
|
|
181
|
+
const stack = [...graph.forward.get(taskId) ?? /* @__PURE__ */ new Set()].filter((depId) => graph.nodes.has(depId));
|
|
182
|
+
while (stack.length > 0) {
|
|
183
|
+
const depId = stack.pop();
|
|
184
|
+
if (!depId || result.has(depId)) continue;
|
|
185
|
+
result.add(depId);
|
|
186
|
+
for (const next of graph.forward.get(depId) ?? /* @__PURE__ */ new Set()) {
|
|
187
|
+
if (graph.nodes.has(next) && !result.has(next)) {
|
|
188
|
+
stack.push(next);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
function transitive_dependents(graph, taskId) {
|
|
195
|
+
const result = /* @__PURE__ */ new Set();
|
|
196
|
+
const stack = [...graph.reverse.get(taskId) ?? /* @__PURE__ */ new Set()].filter((depId) => graph.nodes.has(depId));
|
|
197
|
+
while (stack.length > 0) {
|
|
198
|
+
const dependentId = stack.pop();
|
|
199
|
+
if (!dependentId || result.has(dependentId)) continue;
|
|
200
|
+
result.add(dependentId);
|
|
201
|
+
for (const next of graph.reverse.get(dependentId) ?? /* @__PURE__ */ new Set()) {
|
|
202
|
+
if (graph.nodes.has(next) && !result.has(next)) {
|
|
203
|
+
stack.push(next);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
function build_graph(tasks, context = {}) {
|
|
210
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
211
|
+
for (const task of tasks) {
|
|
212
|
+
if (nodes.has(task.id)) {
|
|
213
|
+
throw new Error(`Duplicate task id '${task.id}' while building graph.`);
|
|
214
|
+
}
|
|
215
|
+
nodes.set(task.id, task);
|
|
216
|
+
}
|
|
217
|
+
const forward = /* @__PURE__ */ new Map();
|
|
218
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const nodeId of nodes.keys()) {
|
|
220
|
+
forward.set(nodeId, /* @__PURE__ */ new Set());
|
|
221
|
+
reverse.set(nodeId, /* @__PURE__ */ new Set());
|
|
222
|
+
}
|
|
223
|
+
for (const task of tasks) {
|
|
224
|
+
const deps = new Set(task.depends_on ?? []);
|
|
225
|
+
forward.set(task.id, deps);
|
|
226
|
+
for (const depId of deps) {
|
|
227
|
+
if (!nodes.has(depId)) continue;
|
|
228
|
+
reverse.get(depId)?.add(task.id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const graph = {
|
|
232
|
+
nodes,
|
|
233
|
+
forward,
|
|
234
|
+
reverse,
|
|
235
|
+
topological_order: [],
|
|
236
|
+
tracks: context.tracks ? new Map(context.tracks) : /* @__PURE__ */ new Map(),
|
|
237
|
+
resources: context.resources ? new Map(context.resources) : /* @__PURE__ */ new Map(),
|
|
238
|
+
deliveries: context.deliveries ? new Map(context.deliveries) : /* @__PURE__ */ new Map()
|
|
239
|
+
};
|
|
240
|
+
graph.topological_order = topological_sort(graph);
|
|
241
|
+
return graph;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/graph/loader.ts
|
|
245
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
246
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
247
|
+
|
|
248
|
+
// src/parser/delivery-parser.ts
|
|
249
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
250
|
+
|
|
251
|
+
// src/models/enums.ts
|
|
252
|
+
var TaskType = {
|
|
253
|
+
feature: "feature",
|
|
254
|
+
bug: "bug",
|
|
255
|
+
chore: "chore",
|
|
256
|
+
spike: "spike",
|
|
257
|
+
epic: "epic"
|
|
258
|
+
};
|
|
259
|
+
var TaskStatus = {
|
|
260
|
+
todo: "todo",
|
|
261
|
+
blocked: "blocked",
|
|
262
|
+
in_progress: "in_progress",
|
|
263
|
+
in_review: "in_review",
|
|
264
|
+
done: "done",
|
|
265
|
+
canceled: "canceled"
|
|
266
|
+
};
|
|
267
|
+
var TaskPriority = {
|
|
268
|
+
p0: "p0",
|
|
269
|
+
p1: "p1",
|
|
270
|
+
p2: "p2",
|
|
271
|
+
p3: "p3"
|
|
272
|
+
};
|
|
273
|
+
var TaskComplexity = {
|
|
274
|
+
trivial: "trivial",
|
|
275
|
+
small: "small",
|
|
276
|
+
medium: "medium",
|
|
277
|
+
large: "large",
|
|
278
|
+
unknown: "unknown"
|
|
279
|
+
};
|
|
280
|
+
var TaskDeterminism = {
|
|
281
|
+
high: "high",
|
|
282
|
+
medium: "medium",
|
|
283
|
+
low: "low",
|
|
284
|
+
experimental: "experimental"
|
|
285
|
+
};
|
|
286
|
+
var IdeaStatus = {
|
|
287
|
+
captured: "captured",
|
|
288
|
+
exploring: "exploring",
|
|
289
|
+
promoted: "promoted",
|
|
290
|
+
parked: "parked",
|
|
291
|
+
rejected: "rejected"
|
|
292
|
+
};
|
|
293
|
+
var DeliveryStatus = {
|
|
294
|
+
planning: "planning",
|
|
295
|
+
committed: "committed",
|
|
296
|
+
in_progress: "in_progress",
|
|
297
|
+
delivered: "delivered",
|
|
298
|
+
canceled: "canceled"
|
|
299
|
+
};
|
|
300
|
+
var ExecutorType = {
|
|
301
|
+
human: "human",
|
|
302
|
+
ai: "ai",
|
|
303
|
+
ci: "ci",
|
|
304
|
+
hybrid: "hybrid"
|
|
305
|
+
};
|
|
306
|
+
var RunbookAction = {
|
|
307
|
+
generate: "generate",
|
|
308
|
+
run: "run",
|
|
309
|
+
review: "review",
|
|
310
|
+
test: "test",
|
|
311
|
+
deploy: "deploy"
|
|
312
|
+
};
|
|
313
|
+
var ArtifactType = {
|
|
314
|
+
pr: "pr",
|
|
315
|
+
migration: "migration",
|
|
316
|
+
deployment: "deployment",
|
|
317
|
+
document: "document",
|
|
318
|
+
config: "config"
|
|
319
|
+
};
|
|
320
|
+
var RiskLevel = {
|
|
321
|
+
low: "low",
|
|
322
|
+
medium: "medium",
|
|
323
|
+
high: "high",
|
|
324
|
+
critical: "critical"
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// src/parser/frontmatter.ts
|
|
328
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
329
|
+
var import_yaml = __toESM(require("yaml"), 1);
|
|
330
|
+
function normalizeInput(content) {
|
|
331
|
+
return content.startsWith("\uFEFF") ? content.slice(1) : content;
|
|
332
|
+
}
|
|
333
|
+
function splitFrontmatter(content, source) {
|
|
334
|
+
const normalized = normalizeInput(content);
|
|
335
|
+
const lines = normalized.split(/\r?\n/);
|
|
336
|
+
if (lines[0] !== "---") {
|
|
337
|
+
throw new Error(`${source}: missing frontmatter opening delimiter '---'.`);
|
|
338
|
+
}
|
|
339
|
+
let closingIndex = -1;
|
|
340
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
341
|
+
if (lines[i] === "---") {
|
|
342
|
+
closingIndex = i;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (closingIndex === -1) {
|
|
347
|
+
throw new Error(`${source}: missing frontmatter closing delimiter '---'.`);
|
|
348
|
+
}
|
|
349
|
+
const yamlBlock = lines.slice(1, closingIndex).join("\n");
|
|
350
|
+
const body = lines.slice(closingIndex + 1).join("\n");
|
|
351
|
+
return { yamlBlock, body };
|
|
352
|
+
}
|
|
353
|
+
function parseYamlObject(yamlBlock, source) {
|
|
354
|
+
let parsed;
|
|
355
|
+
try {
|
|
356
|
+
parsed = import_yaml.default.parse(yamlBlock);
|
|
357
|
+
} catch (error6) {
|
|
358
|
+
const message = error6 instanceof Error ? error6.message : String(error6);
|
|
359
|
+
throw new Error(`${source}: invalid YAML frontmatter: ${message}`);
|
|
360
|
+
}
|
|
361
|
+
if (parsed == null) {
|
|
362
|
+
return {};
|
|
363
|
+
}
|
|
364
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
365
|
+
throw new Error(`${source}: frontmatter must parse to a YAML mapping object.`);
|
|
366
|
+
}
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
function parseFrontmatterContent(content, source = "<content>") {
|
|
370
|
+
const { yamlBlock, body } = splitFrontmatter(content, source);
|
|
371
|
+
const frontmatter = parseYamlObject(yamlBlock, source);
|
|
372
|
+
return { frontmatter, body };
|
|
373
|
+
}
|
|
374
|
+
function parseFrontmatterFile(filePath) {
|
|
375
|
+
const content = import_node_fs.default.readFileSync(filePath, "utf8");
|
|
376
|
+
return parseFrontmatterContent(content, filePath);
|
|
377
|
+
}
|
|
378
|
+
function stringifyFrontmatter(frontmatter, body = "") {
|
|
379
|
+
const yamlText = import_yaml.default.stringify(frontmatter).trimEnd();
|
|
380
|
+
if (body.length > 0) {
|
|
381
|
+
return `---
|
|
382
|
+
${yamlText}
|
|
383
|
+
---
|
|
384
|
+
${body}`;
|
|
385
|
+
}
|
|
386
|
+
return `---
|
|
387
|
+
${yamlText}
|
|
388
|
+
---
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/parser/yaml-parser.ts
|
|
393
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
394
|
+
var import_yaml2 = __toESM(require("yaml"), 1);
|
|
395
|
+
function parseYamlContent(content, source = "<content>") {
|
|
396
|
+
let parsed;
|
|
397
|
+
try {
|
|
398
|
+
parsed = import_yaml2.default.parse(content);
|
|
399
|
+
} catch (error6) {
|
|
400
|
+
const message = error6 instanceof Error ? error6.message : String(error6);
|
|
401
|
+
throw new Error(`${source}: invalid YAML: ${message}`);
|
|
402
|
+
}
|
|
403
|
+
if (parsed == null) {
|
|
404
|
+
return {};
|
|
405
|
+
}
|
|
406
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
407
|
+
throw new Error(`${source}: YAML content must parse to a mapping object.`);
|
|
408
|
+
}
|
|
409
|
+
return parsed;
|
|
410
|
+
}
|
|
411
|
+
function parseYamlFile(filePath) {
|
|
412
|
+
const content = import_node_fs2.default.readFileSync(filePath, "utf8");
|
|
413
|
+
return parseYamlContent(content, filePath);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/parser/delivery-parser.ts
|
|
417
|
+
function asStringArray(value) {
|
|
418
|
+
if (!Array.isArray(value)) {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
return value.map((entry) => String(entry));
|
|
422
|
+
}
|
|
423
|
+
function parseDeliveryFromRaw(raw, source) {
|
|
424
|
+
const id = raw.id;
|
|
425
|
+
const name = raw.name;
|
|
426
|
+
const status = raw.status;
|
|
427
|
+
if (typeof id !== "string" || !id.trim()) throw new Error(`${source}: missing or invalid required field 'id'.`);
|
|
428
|
+
if (typeof name !== "string" || !name.trim()) throw new Error(`${source}: missing or invalid required field 'name'.`);
|
|
429
|
+
if (typeof status !== "string" || !Object.values(DeliveryStatus).includes(status)) {
|
|
430
|
+
throw new Error(`${source}: invalid delivery status '${String(status)}'.`);
|
|
431
|
+
}
|
|
432
|
+
const scopeRaw = raw.scope;
|
|
433
|
+
const scopeRecord = typeof scopeRaw === "object" && scopeRaw !== null ? scopeRaw : {};
|
|
434
|
+
return {
|
|
435
|
+
id,
|
|
436
|
+
name,
|
|
437
|
+
status,
|
|
438
|
+
target_date: typeof raw.target_date === "string" ? raw.target_date : null,
|
|
439
|
+
started_date: typeof raw.started_date === "string" ? raw.started_date : null,
|
|
440
|
+
delivered_date: typeof raw.delivered_date === "string" ? raw.delivered_date : null,
|
|
441
|
+
budget: typeof raw.budget === "object" && raw.budget !== null ? raw.budget : {},
|
|
442
|
+
capacity_profiles: asStringArray(raw.capacity_profiles),
|
|
443
|
+
scope: {
|
|
444
|
+
include: asStringArray(scopeRecord.include),
|
|
445
|
+
exclude: asStringArray(scopeRecord.exclude)
|
|
446
|
+
},
|
|
447
|
+
risks: Array.isArray(raw.risks) ? raw.risks : void 0,
|
|
448
|
+
governance: typeof raw.governance === "object" && raw.governance !== null ? raw.governance : void 0
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function parseDeliveryContentRaw(content, source) {
|
|
452
|
+
const trimmed = content.trimStart();
|
|
453
|
+
if (trimmed.startsWith("---")) {
|
|
454
|
+
return parseFrontmatterContent(content, source);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
frontmatter: parseYamlContent(content, source),
|
|
458
|
+
body: ""
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function parseDeliveryContent(content, source = "<content>") {
|
|
462
|
+
const { frontmatter, body } = parseDeliveryContentRaw(content, source);
|
|
463
|
+
return {
|
|
464
|
+
delivery: parseDeliveryFromRaw(frontmatter, source),
|
|
465
|
+
body,
|
|
466
|
+
raw: frontmatter
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function parseDeliveryFile(filePath) {
|
|
470
|
+
const content = import_node_fs3.default.readFileSync(filePath, "utf8");
|
|
471
|
+
return parseDeliveryContent(content, filePath);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/parser/task-parser.ts
|
|
475
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
476
|
+
var REQUIRED_LAYER_A_FIELDS = ["id", "title", "type", "status", "created", "updated"];
|
|
477
|
+
function readStringField(raw, field, source) {
|
|
478
|
+
const value = raw[field];
|
|
479
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
480
|
+
throw new Error(`${source}: missing or invalid required field '${field}'.`);
|
|
481
|
+
}
|
|
482
|
+
return value;
|
|
483
|
+
}
|
|
484
|
+
function validateLayerA(raw, source) {
|
|
485
|
+
for (const field of REQUIRED_LAYER_A_FIELDS) {
|
|
486
|
+
readStringField(raw, field, source);
|
|
487
|
+
}
|
|
488
|
+
const type = readStringField(raw, "type", source);
|
|
489
|
+
if (!Object.values(TaskType).includes(type)) {
|
|
490
|
+
throw new Error(`${source}: invalid task type '${type}'.`);
|
|
491
|
+
}
|
|
492
|
+
const status = readStringField(raw, "status", source);
|
|
493
|
+
if (!Object.values(TaskStatus).includes(status)) {
|
|
494
|
+
throw new Error(`${source}: invalid task status '${status}'.`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function parseTaskContent(content, source = "<content>") {
|
|
498
|
+
const { frontmatter, body } = parseFrontmatterContent(content, source);
|
|
499
|
+
validateLayerA(frontmatter, source);
|
|
500
|
+
return {
|
|
501
|
+
task: frontmatter,
|
|
502
|
+
body,
|
|
503
|
+
raw: frontmatter
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function parseTaskFile(filePath) {
|
|
507
|
+
const content = import_node_fs4.default.readFileSync(filePath, "utf8");
|
|
508
|
+
return parseTaskContent(content, filePath);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/graph/loader.ts
|
|
512
|
+
function walkFiles(dirPath, extensions) {
|
|
513
|
+
if (!import_node_fs5.default.existsSync(dirPath)) return [];
|
|
514
|
+
const out = [];
|
|
515
|
+
const entries = import_node_fs5.default.readdirSync(dirPath, { withFileTypes: true });
|
|
516
|
+
for (const entry of entries) {
|
|
517
|
+
const fullPath = import_node_path.default.join(dirPath, entry.name);
|
|
518
|
+
if (entry.isDirectory()) {
|
|
519
|
+
out.push(...walkFiles(fullPath, extensions));
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (!entry.isFile()) continue;
|
|
523
|
+
const ext = import_node_path.default.extname(entry.name).toLowerCase();
|
|
524
|
+
if (extensions.has(ext)) {
|
|
525
|
+
out.push(fullPath);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
529
|
+
}
|
|
530
|
+
function loadTasks(tasksDir) {
|
|
531
|
+
const files = walkFiles(tasksDir, /* @__PURE__ */ new Set([".md"]));
|
|
532
|
+
const tasks = [];
|
|
533
|
+
const seen = /* @__PURE__ */ new Set();
|
|
534
|
+
for (const filePath of files) {
|
|
535
|
+
const parsed = parseTaskFile(filePath);
|
|
536
|
+
if (seen.has(parsed.task.id)) {
|
|
537
|
+
throw new Error(`Duplicate task id '${parsed.task.id}' found at ${filePath}.`);
|
|
538
|
+
}
|
|
539
|
+
seen.add(parsed.task.id);
|
|
540
|
+
tasks.push(parsed.task);
|
|
541
|
+
}
|
|
542
|
+
return tasks;
|
|
543
|
+
}
|
|
544
|
+
function loadTracks(tracksDir) {
|
|
545
|
+
const files = walkFiles(tracksDir, /* @__PURE__ */ new Set([".yml", ".yaml"]));
|
|
546
|
+
const tracks = /* @__PURE__ */ new Map();
|
|
547
|
+
for (const filePath of files) {
|
|
548
|
+
const data = parseYamlFile(filePath);
|
|
549
|
+
if (!data.id) continue;
|
|
550
|
+
tracks.set(data.id, data);
|
|
551
|
+
}
|
|
552
|
+
return tracks;
|
|
553
|
+
}
|
|
554
|
+
function loadResources(resourcesDir) {
|
|
555
|
+
const files = walkFiles(resourcesDir, /* @__PURE__ */ new Set([".yml", ".yaml"]));
|
|
556
|
+
const resources = /* @__PURE__ */ new Map();
|
|
557
|
+
for (const filePath of files) {
|
|
558
|
+
const data = parseYamlFile(filePath);
|
|
559
|
+
if (!data.id) continue;
|
|
560
|
+
resources.set(data.id, data);
|
|
561
|
+
}
|
|
562
|
+
return resources;
|
|
563
|
+
}
|
|
564
|
+
function loadDeliveries(deliveriesDir) {
|
|
565
|
+
const files = walkFiles(deliveriesDir, /* @__PURE__ */ new Set([".yml", ".yaml", ".md"]));
|
|
566
|
+
const deliveries = /* @__PURE__ */ new Map();
|
|
567
|
+
for (const filePath of files) {
|
|
568
|
+
const parsed = parseDeliveryFile(filePath);
|
|
569
|
+
deliveries.set(parsed.delivery.id, parsed.delivery);
|
|
570
|
+
}
|
|
571
|
+
return deliveries;
|
|
572
|
+
}
|
|
573
|
+
function load_graph(coopDir) {
|
|
574
|
+
const tasks = loadTasks(import_node_path.default.join(coopDir, "tasks"));
|
|
575
|
+
const tracks = loadTracks(import_node_path.default.join(coopDir, "tracks"));
|
|
576
|
+
const resources = loadResources(import_node_path.default.join(coopDir, "resources"));
|
|
577
|
+
const deliveries = loadDeliveries(import_node_path.default.join(coopDir, "deliveries"));
|
|
578
|
+
return build_graph(tasks, {
|
|
579
|
+
tracks,
|
|
580
|
+
resources,
|
|
581
|
+
deliveries
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/graph/subgraph.ts
|
|
586
|
+
function extract_subgraph(graph, taskIds) {
|
|
587
|
+
const selected = new Set(taskIds.filter((taskId) => graph.nodes.has(taskId)));
|
|
588
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const taskId of selected) {
|
|
590
|
+
const task = graph.nodes.get(taskId);
|
|
591
|
+
if (task) nodes.set(taskId, task);
|
|
592
|
+
}
|
|
593
|
+
const forward = /* @__PURE__ */ new Map();
|
|
594
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
595
|
+
for (const taskId of selected) {
|
|
596
|
+
forward.set(taskId, /* @__PURE__ */ new Set());
|
|
597
|
+
reverse.set(taskId, /* @__PURE__ */ new Set());
|
|
598
|
+
}
|
|
599
|
+
for (const taskId of selected) {
|
|
600
|
+
const deps = graph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
601
|
+
const internalDeps = new Set(Array.from(deps).filter((depId) => selected.has(depId)));
|
|
602
|
+
forward.set(taskId, internalDeps);
|
|
603
|
+
for (const depId of internalDeps) {
|
|
604
|
+
reverse.get(depId)?.add(taskId);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const subgraph = {
|
|
608
|
+
nodes,
|
|
609
|
+
forward,
|
|
610
|
+
reverse,
|
|
611
|
+
topological_order: [],
|
|
612
|
+
tracks: new Map(graph.tracks),
|
|
613
|
+
resources: new Map(graph.resources),
|
|
614
|
+
deliveries: new Map(graph.deliveries)
|
|
615
|
+
};
|
|
616
|
+
subgraph.topological_order = topological_sort(subgraph);
|
|
617
|
+
return subgraph;
|
|
618
|
+
}
|
|
619
|
+
function find_external_dependencies(subgraph, fullGraph) {
|
|
620
|
+
const external = /* @__PURE__ */ new Set();
|
|
621
|
+
for (const taskId of subgraph.nodes.keys()) {
|
|
622
|
+
const deps = fullGraph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
623
|
+
for (const depId of deps) {
|
|
624
|
+
if (!subgraph.nodes.has(depId) && fullGraph.nodes.has(depId)) {
|
|
625
|
+
external.add(depId);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return Array.from(external).sort((a, b) => a.localeCompare(b));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/graph/validator.ts
|
|
633
|
+
function error(invariant, message, task_ids) {
|
|
634
|
+
return {
|
|
635
|
+
level: "error",
|
|
636
|
+
invariant,
|
|
637
|
+
message,
|
|
638
|
+
task_ids
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function buildUndirectedAdjacency(graph) {
|
|
642
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
643
|
+
for (const nodeId of graph.nodes.keys()) {
|
|
644
|
+
adjacency.set(nodeId, /* @__PURE__ */ new Set());
|
|
645
|
+
}
|
|
646
|
+
for (const [nodeId, deps] of graph.forward.entries()) {
|
|
647
|
+
for (const depId of deps) {
|
|
648
|
+
if (!graph.nodes.has(depId)) continue;
|
|
649
|
+
adjacency.get(nodeId)?.add(depId);
|
|
650
|
+
adjacency.get(depId)?.add(nodeId);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return adjacency;
|
|
654
|
+
}
|
|
655
|
+
function connectedComponents(graph) {
|
|
656
|
+
const adjacency = buildUndirectedAdjacency(graph);
|
|
657
|
+
const visited = /* @__PURE__ */ new Set();
|
|
658
|
+
const components = [];
|
|
659
|
+
for (const nodeId of graph.nodes.keys()) {
|
|
660
|
+
if (visited.has(nodeId)) continue;
|
|
661
|
+
const component = [];
|
|
662
|
+
const stack = [nodeId];
|
|
663
|
+
visited.add(nodeId);
|
|
664
|
+
while (stack.length > 0) {
|
|
665
|
+
const current = stack.pop();
|
|
666
|
+
if (!current) continue;
|
|
667
|
+
component.push(current);
|
|
668
|
+
for (const next of adjacency.get(current) ?? /* @__PURE__ */ new Set()) {
|
|
669
|
+
if (visited.has(next)) continue;
|
|
670
|
+
visited.add(next);
|
|
671
|
+
stack.push(next);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
components.push(component);
|
|
675
|
+
}
|
|
676
|
+
return components;
|
|
677
|
+
}
|
|
678
|
+
function checkTerminalConvergence(graph) {
|
|
679
|
+
const issues = [];
|
|
680
|
+
const components = connectedComponents(graph);
|
|
681
|
+
for (const component of components) {
|
|
682
|
+
const hasLeaf = component.some((nodeId) => (graph.reverse.get(nodeId)?.size ?? 0) === 0);
|
|
683
|
+
if (!hasLeaf) {
|
|
684
|
+
issues.push(
|
|
685
|
+
error(
|
|
686
|
+
"terminal_convergence",
|
|
687
|
+
`Connected component lacks a leaf task (zero dependents): ${component.join(", ")}.`,
|
|
688
|
+
component
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return issues;
|
|
694
|
+
}
|
|
695
|
+
function validate_graph(graph) {
|
|
696
|
+
const issues = [];
|
|
697
|
+
const cycle = detect_cycle(graph);
|
|
698
|
+
if (cycle) {
|
|
699
|
+
issues.push(error("acyclicity", `Dependency cycle detected: ${cycle.join(" -> ")}.`, cycle));
|
|
700
|
+
}
|
|
701
|
+
for (const [taskId, deps] of graph.forward.entries()) {
|
|
702
|
+
for (const depId of deps) {
|
|
703
|
+
if (!graph.nodes.has(depId)) {
|
|
704
|
+
issues.push(
|
|
705
|
+
error(
|
|
706
|
+
"referential_integrity",
|
|
707
|
+
`Task '${taskId}' references missing dependency '${depId}'.`,
|
|
708
|
+
[taskId, depId]
|
|
709
|
+
)
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
issues.push(...checkTerminalConvergence(graph));
|
|
715
|
+
for (const [taskId, task] of graph.nodes.entries()) {
|
|
716
|
+
if (task.status !== "done") continue;
|
|
717
|
+
const deps = graph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
718
|
+
const unresolved = Array.from(deps).filter((depId) => {
|
|
719
|
+
const depTask = graph.nodes.get(depId);
|
|
720
|
+
if (!depTask) return false;
|
|
721
|
+
return depTask.status !== "done";
|
|
722
|
+
});
|
|
723
|
+
if (unresolved.length > 0) {
|
|
724
|
+
issues.push(
|
|
725
|
+
error(
|
|
726
|
+
"status_consistency",
|
|
727
|
+
`Task '${taskId}' is done but has dependencies not done: ${unresolved.join(", ")}.`,
|
|
728
|
+
[taskId, ...unresolved]
|
|
729
|
+
)
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return issues;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/parser/idea-parser.ts
|
|
737
|
+
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
738
|
+
function asStringArray2(value) {
|
|
739
|
+
if (!Array.isArray(value)) {
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
return value.map((entry) => String(entry));
|
|
743
|
+
}
|
|
744
|
+
function parseIdeaFromRaw(raw, source) {
|
|
745
|
+
const id = raw.id;
|
|
746
|
+
const title = raw.title;
|
|
747
|
+
const created = raw.created;
|
|
748
|
+
const author = raw.author;
|
|
749
|
+
const status = raw.status;
|
|
750
|
+
const sourceField = raw.source;
|
|
751
|
+
if (typeof id !== "string" || !id.trim()) throw new Error(`${source}: missing or invalid required field 'id'.`);
|
|
752
|
+
if (typeof title !== "string" || !title.trim()) throw new Error(`${source}: missing or invalid required field 'title'.`);
|
|
753
|
+
if (typeof created !== "string" || !created.trim()) {
|
|
754
|
+
throw new Error(`${source}: missing or invalid required field 'created'.`);
|
|
755
|
+
}
|
|
756
|
+
if (typeof author !== "string" || !author.trim()) {
|
|
757
|
+
throw new Error(`${source}: missing or invalid required field 'author'.`);
|
|
758
|
+
}
|
|
759
|
+
if (typeof sourceField !== "string" || !sourceField.trim()) {
|
|
760
|
+
throw new Error(`${source}: missing or invalid required field 'source'.`);
|
|
761
|
+
}
|
|
762
|
+
if (typeof status !== "string" || !Object.values(IdeaStatus).includes(status)) {
|
|
763
|
+
throw new Error(`${source}: invalid idea status '${String(status)}'.`);
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
id,
|
|
767
|
+
title,
|
|
768
|
+
created,
|
|
769
|
+
author,
|
|
770
|
+
status,
|
|
771
|
+
tags: asStringArray2(raw.tags),
|
|
772
|
+
source: sourceField,
|
|
773
|
+
linked_tasks: asStringArray2(raw.linked_tasks)
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function parseIdeaContent(content, source = "<content>") {
|
|
777
|
+
const { frontmatter, body } = parseFrontmatterContent(content, source);
|
|
778
|
+
return {
|
|
779
|
+
idea: parseIdeaFromRaw(frontmatter, source),
|
|
780
|
+
body,
|
|
781
|
+
raw: frontmatter
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function parseIdeaFile(filePath) {
|
|
785
|
+
const content = import_node_fs6.default.readFileSync(filePath, "utf8");
|
|
786
|
+
return parseIdeaContent(content, filePath);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/parser/task-writer.ts
|
|
790
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
791
|
+
var TASK_FIELD_ORDER = [
|
|
792
|
+
"id",
|
|
793
|
+
"title",
|
|
794
|
+
"type",
|
|
795
|
+
"status",
|
|
796
|
+
"created",
|
|
797
|
+
"updated",
|
|
798
|
+
"priority",
|
|
799
|
+
"track",
|
|
800
|
+
"assignee",
|
|
801
|
+
"depends_on",
|
|
802
|
+
"tags",
|
|
803
|
+
"delivery",
|
|
804
|
+
"complexity",
|
|
805
|
+
"determinism",
|
|
806
|
+
"estimate",
|
|
807
|
+
"resources",
|
|
808
|
+
"execution",
|
|
809
|
+
"artifacts",
|
|
810
|
+
"governance",
|
|
811
|
+
"risk",
|
|
812
|
+
"metrics",
|
|
813
|
+
"enterprise"
|
|
814
|
+
];
|
|
815
|
+
var ORDERED_TASK_FIELDS = new Set(TASK_FIELD_ORDER);
|
|
816
|
+
function buildOrderedFrontmatter(task, raw) {
|
|
817
|
+
const taskRecord = task;
|
|
818
|
+
const ordered = {};
|
|
819
|
+
for (const key of TASK_FIELD_ORDER) {
|
|
820
|
+
const fromTask = taskRecord[key];
|
|
821
|
+
if (fromTask !== void 0) {
|
|
822
|
+
ordered[key] = fromTask;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (raw && Object.prototype.hasOwnProperty.call(raw, key)) {
|
|
826
|
+
const fromRaw = raw[key];
|
|
827
|
+
if (fromRaw !== void 0) {
|
|
828
|
+
ordered[key] = fromRaw;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (!raw) {
|
|
833
|
+
return ordered;
|
|
834
|
+
}
|
|
835
|
+
for (const key of Object.keys(raw)) {
|
|
836
|
+
if (ORDERED_TASK_FIELDS.has(key)) {
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
ordered[key] = raw[key];
|
|
840
|
+
}
|
|
841
|
+
return ordered;
|
|
842
|
+
}
|
|
843
|
+
function writeTask(task, options = {}) {
|
|
844
|
+
const frontmatter = buildOrderedFrontmatter(task, options.raw);
|
|
845
|
+
const output = stringifyFrontmatter(frontmatter, options.body ?? "");
|
|
846
|
+
if (options.filePath) {
|
|
847
|
+
import_node_fs7.default.writeFileSync(options.filePath, output, "utf8");
|
|
848
|
+
}
|
|
849
|
+
return output;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/planning/readiness.ts
|
|
853
|
+
function isResolvedDependencyStatus(status) {
|
|
854
|
+
return status === "done" || status === "canceled";
|
|
855
|
+
}
|
|
856
|
+
function compute_readiness(task, graph) {
|
|
857
|
+
if (task.status === "done" || task.status === "canceled") {
|
|
858
|
+
return "done";
|
|
859
|
+
}
|
|
860
|
+
if (task.status === "in_review") {
|
|
861
|
+
return "waiting_review";
|
|
862
|
+
}
|
|
863
|
+
if (task.status === "in_progress") {
|
|
864
|
+
return "in_progress";
|
|
865
|
+
}
|
|
866
|
+
for (const depId of task.depends_on ?? []) {
|
|
867
|
+
const dep = graph.nodes.get(depId);
|
|
868
|
+
if (!dep || !isResolvedDependencyStatus(dep.status)) {
|
|
869
|
+
return "blocked";
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return "ready";
|
|
873
|
+
}
|
|
874
|
+
function compute_all_readiness(graph) {
|
|
875
|
+
const readiness = /* @__PURE__ */ new Map();
|
|
876
|
+
for (const [taskId, task] of graph.nodes.entries()) {
|
|
877
|
+
readiness.set(taskId, compute_readiness(task, graph));
|
|
878
|
+
}
|
|
879
|
+
return readiness;
|
|
880
|
+
}
|
|
881
|
+
function partition_by_readiness(graph) {
|
|
882
|
+
const partitions = {
|
|
883
|
+
ready: [],
|
|
884
|
+
blocked: [],
|
|
885
|
+
in_progress: [],
|
|
886
|
+
waiting_review: [],
|
|
887
|
+
done: []
|
|
888
|
+
};
|
|
889
|
+
for (const task of graph.nodes.values()) {
|
|
890
|
+
const state = compute_readiness(task, graph);
|
|
891
|
+
partitions[state].push(task);
|
|
892
|
+
}
|
|
893
|
+
return partitions;
|
|
894
|
+
}
|
|
895
|
+
function compute_readiness_with_corrections(graph) {
|
|
896
|
+
const readiness = /* @__PURE__ */ new Map();
|
|
897
|
+
const corrections = [];
|
|
898
|
+
const warnings = [];
|
|
899
|
+
const partitions = {
|
|
900
|
+
ready: [],
|
|
901
|
+
blocked: [],
|
|
902
|
+
in_progress: [],
|
|
903
|
+
waiting_review: [],
|
|
904
|
+
done: []
|
|
905
|
+
};
|
|
906
|
+
for (const [taskId, task] of graph.nodes.entries()) {
|
|
907
|
+
const state = compute_readiness(task, graph);
|
|
908
|
+
readiness.set(taskId, state);
|
|
909
|
+
partitions[state].push(task);
|
|
910
|
+
if (task.status === "blocked" && state === "ready") {
|
|
911
|
+
corrections.push({
|
|
912
|
+
type: "task.transitioned",
|
|
913
|
+
task_id: taskId,
|
|
914
|
+
from: "blocked",
|
|
915
|
+
to: "todo",
|
|
916
|
+
reason: "All dependencies are resolved."
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
if (task.status === "todo" && state === "blocked") {
|
|
920
|
+
const unresolved = (task.depends_on ?? []).filter((depId) => {
|
|
921
|
+
const depTask = graph.nodes.get(depId);
|
|
922
|
+
return !depTask || !isResolvedDependencyStatus(depTask.status);
|
|
923
|
+
});
|
|
924
|
+
warnings.push({
|
|
925
|
+
code: "todo_with_unresolved_dependencies",
|
|
926
|
+
task_id: taskId,
|
|
927
|
+
message: `Task is todo but has unresolved dependencies: ${unresolved.join(", ")}.`
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
readiness,
|
|
933
|
+
partitions,
|
|
934
|
+
corrections,
|
|
935
|
+
warnings
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/schema/version.ts
|
|
940
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
941
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
942
|
+
var CURRENT_SCHEMA_VERSION = 2;
|
|
943
|
+
function schemaVersionFile(coopDir) {
|
|
944
|
+
return import_node_path2.default.join(coopDir, "schema-version");
|
|
945
|
+
}
|
|
946
|
+
function read_schema_version(coopDir) {
|
|
947
|
+
const filePath = schemaVersionFile(coopDir);
|
|
948
|
+
if (!import_node_fs8.default.existsSync(filePath)) {
|
|
949
|
+
throw new Error(`Missing schema-version file at ${filePath}.`);
|
|
950
|
+
}
|
|
951
|
+
const raw = import_node_fs8.default.readFileSync(filePath, "utf8").trim();
|
|
952
|
+
if (!raw) {
|
|
953
|
+
throw new Error(`Schema version file is empty at ${filePath}.`);
|
|
954
|
+
}
|
|
955
|
+
const version = Number(raw);
|
|
956
|
+
if (!Number.isInteger(version) || version <= 0) {
|
|
957
|
+
throw new Error(`Invalid schema version '${raw}' in ${filePath}.`);
|
|
958
|
+
}
|
|
959
|
+
return version;
|
|
960
|
+
}
|
|
961
|
+
function write_schema_version(coopDir, version) {
|
|
962
|
+
if (!Number.isInteger(version) || version <= 0) {
|
|
963
|
+
throw new Error(`Schema version must be a positive integer. Received: ${String(version)}.`);
|
|
964
|
+
}
|
|
965
|
+
const filePath = schemaVersionFile(coopDir);
|
|
966
|
+
import_node_fs8.default.writeFileSync(filePath, `${version}
|
|
967
|
+
`, "utf8");
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/state/auto-transitions.ts
|
|
971
|
+
function toStatusMap(value) {
|
|
972
|
+
if (!value) return /* @__PURE__ */ new Map();
|
|
973
|
+
if (value instanceof Map) return new Map(value);
|
|
974
|
+
return new Map(Object.entries(value));
|
|
975
|
+
}
|
|
976
|
+
function unresolvedDependencies(task, dependencyStatuses) {
|
|
977
|
+
const statuses = toStatusMap(dependencyStatuses);
|
|
978
|
+
return (task.depends_on ?? []).filter((depId) => {
|
|
979
|
+
const status = statuses.get(depId);
|
|
980
|
+
return status !== "done" && status !== "canceled";
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
function check_blocked(task, dependencyStatuses) {
|
|
984
|
+
if (task.status === "done" || task.status === "canceled" || task.status === "blocked") {
|
|
985
|
+
return { should_transition: false };
|
|
986
|
+
}
|
|
987
|
+
const unresolved = unresolvedDependencies(task, dependencyStatuses);
|
|
988
|
+
if (unresolved.length === 0) {
|
|
989
|
+
return { should_transition: false };
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
should_transition: true,
|
|
993
|
+
target_status: "blocked",
|
|
994
|
+
event: {
|
|
995
|
+
type: "task.transitioned",
|
|
996
|
+
automated: true,
|
|
997
|
+
task_id: task.id,
|
|
998
|
+
from: task.status,
|
|
999
|
+
to: "blocked",
|
|
1000
|
+
reason: `Unresolved dependencies detected: ${unresolved.join(", ")}.`
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function check_unblocked(task, dependencyStatuses) {
|
|
1005
|
+
if (task.status !== "blocked") {
|
|
1006
|
+
return { should_transition: false };
|
|
1007
|
+
}
|
|
1008
|
+
const unresolved = unresolvedDependencies(task, dependencyStatuses);
|
|
1009
|
+
if (unresolved.length > 0) {
|
|
1010
|
+
return { should_transition: false };
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
should_transition: true,
|
|
1014
|
+
target_status: "todo",
|
|
1015
|
+
event: {
|
|
1016
|
+
type: "task.transitioned",
|
|
1017
|
+
automated: true,
|
|
1018
|
+
task_id: task.id,
|
|
1019
|
+
from: "blocked",
|
|
1020
|
+
to: "todo",
|
|
1021
|
+
reason: "All dependencies are resolved."
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/state/machine.ts
|
|
1027
|
+
var VALID_TASK_TRANSITIONS = /* @__PURE__ */ new Map([
|
|
1028
|
+
["todo", /* @__PURE__ */ new Set(["in_progress", "blocked", "canceled"])],
|
|
1029
|
+
["blocked", /* @__PURE__ */ new Set(["todo"])],
|
|
1030
|
+
["in_progress", /* @__PURE__ */ new Set(["in_review", "blocked", "todo", "canceled"])],
|
|
1031
|
+
["in_review", /* @__PURE__ */ new Set(["done", "todo"])],
|
|
1032
|
+
["done", /* @__PURE__ */ new Set()],
|
|
1033
|
+
["canceled", /* @__PURE__ */ new Set(["todo"])]
|
|
1034
|
+
]);
|
|
1035
|
+
function toStatusMap2(value) {
|
|
1036
|
+
if (!value) return /* @__PURE__ */ new Map();
|
|
1037
|
+
if (value instanceof Map) return new Map(value);
|
|
1038
|
+
return new Map(Object.entries(value));
|
|
1039
|
+
}
|
|
1040
|
+
function toIsoDate(now) {
|
|
1041
|
+
if (typeof now === "string") {
|
|
1042
|
+
return now;
|
|
1043
|
+
}
|
|
1044
|
+
const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
|
|
1045
|
+
return date.toISOString().slice(0, 10);
|
|
1046
|
+
}
|
|
1047
|
+
function validateGovernanceAndDeps(task, targetStatus, context) {
|
|
1048
|
+
if (task.status === "in_review" && targetStatus === "done" && task.governance?.approval_required) {
|
|
1049
|
+
const reviewer = task.governance.reviewer;
|
|
1050
|
+
if (!reviewer || context.actor !== reviewer) {
|
|
1051
|
+
return "Approval-required transition in_review -> done must be performed by the configured reviewer.";
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (task.status === "blocked" && targetStatus === "todo") {
|
|
1055
|
+
const depStatuses = toStatusMap2(context.dependencyStatuses);
|
|
1056
|
+
const unresolved = (task.depends_on ?? []).filter((depId) => {
|
|
1057
|
+
const status = depStatuses.get(depId);
|
|
1058
|
+
return status !== "done" && status !== "canceled";
|
|
1059
|
+
});
|
|
1060
|
+
if (unresolved.length > 0) {
|
|
1061
|
+
return `blocked -> todo requires all dependencies resolved. Pending: ${unresolved.join(", ")}.`;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
function transition(task, targetStatus, context = {}) {
|
|
1067
|
+
const allowed = VALID_TASK_TRANSITIONS.get(task.status) ?? /* @__PURE__ */ new Set();
|
|
1068
|
+
if (!allowed.has(targetStatus)) {
|
|
1069
|
+
return {
|
|
1070
|
+
success: false,
|
|
1071
|
+
task,
|
|
1072
|
+
error: `Invalid transition ${task.status} -> ${targetStatus}. Allowed: ${Array.from(allowed).join(", ") || "none"}.`
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
const governanceError = validateGovernanceAndDeps(task, targetStatus, context);
|
|
1076
|
+
if (governanceError) {
|
|
1077
|
+
return {
|
|
1078
|
+
success: false,
|
|
1079
|
+
task,
|
|
1080
|
+
error: governanceError
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
success: true,
|
|
1085
|
+
task: {
|
|
1086
|
+
...task,
|
|
1087
|
+
status: targetStatus,
|
|
1088
|
+
updated: toIsoDate(context.now)
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/validator/referential.ts
|
|
1094
|
+
function error2(field, rule, message) {
|
|
1095
|
+
return { level: "error", field, rule, message };
|
|
1096
|
+
}
|
|
1097
|
+
function warning(field, rule, message) {
|
|
1098
|
+
return { level: "warning", field, rule, message };
|
|
1099
|
+
}
|
|
1100
|
+
function toDependencyMap(input) {
|
|
1101
|
+
if (!input) {
|
|
1102
|
+
return /* @__PURE__ */ new Map();
|
|
1103
|
+
}
|
|
1104
|
+
if (input instanceof Map) {
|
|
1105
|
+
return new Map(input);
|
|
1106
|
+
}
|
|
1107
|
+
const map = /* @__PURE__ */ new Map();
|
|
1108
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1109
|
+
map.set(key, Array.isArray(value) ? value : []);
|
|
1110
|
+
}
|
|
1111
|
+
return map;
|
|
1112
|
+
}
|
|
1113
|
+
function hasCycle(graph) {
|
|
1114
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1115
|
+
const active = /* @__PURE__ */ new Set();
|
|
1116
|
+
const dfs = (node) => {
|
|
1117
|
+
if (active.has(node)) return true;
|
|
1118
|
+
if (visited.has(node)) return false;
|
|
1119
|
+
visited.add(node);
|
|
1120
|
+
active.add(node);
|
|
1121
|
+
const deps = graph.get(node) ?? [];
|
|
1122
|
+
for (const dep of deps) {
|
|
1123
|
+
if (dfs(dep)) return true;
|
|
1124
|
+
}
|
|
1125
|
+
active.delete(node);
|
|
1126
|
+
return false;
|
|
1127
|
+
};
|
|
1128
|
+
for (const node of graph.keys()) {
|
|
1129
|
+
if (dfs(node)) return true;
|
|
1130
|
+
}
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
function validateReferential(task, context = {}) {
|
|
1134
|
+
const issues = [];
|
|
1135
|
+
const existingIds = context.existingIds ?? /* @__PURE__ */ new Set();
|
|
1136
|
+
const currentTaskId = context.currentTaskId ?? null;
|
|
1137
|
+
if (task.id && existingIds.has(task.id) && (currentTaskId === null || currentTaskId !== task.id)) {
|
|
1138
|
+
issues.push(error2("id", "ref.id_unique", `Task id '${task.id}' must be unique.`));
|
|
1139
|
+
}
|
|
1140
|
+
const dependsOn = task.depends_on ?? [];
|
|
1141
|
+
for (const depId of dependsOn) {
|
|
1142
|
+
if (!existingIds.has(depId)) {
|
|
1143
|
+
issues.push(
|
|
1144
|
+
error2(
|
|
1145
|
+
"depends_on",
|
|
1146
|
+
"ref.depends_on_exists",
|
|
1147
|
+
`Dependency '${depId}' does not reference an existing task.`
|
|
1148
|
+
)
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const graph = toDependencyMap(context.dependencyMap);
|
|
1153
|
+
if (task.id) {
|
|
1154
|
+
graph.set(task.id, dependsOn);
|
|
1155
|
+
}
|
|
1156
|
+
if (graph.size > 0 && hasCycle(graph)) {
|
|
1157
|
+
issues.push(error2("depends_on", "ref.no_cycles", "Dependency graph contains a cycle."));
|
|
1158
|
+
}
|
|
1159
|
+
if (task.track && task.track !== "unassigned" && context.tracks && !context.tracks.has(task.track)) {
|
|
1160
|
+
issues.push(
|
|
1161
|
+
warning("track", "ref.track_exists", `Track '${task.track}' does not match a known track (or 'unassigned').`)
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
if (task.delivery && context.deliveries && !context.deliveries.has(task.delivery)) {
|
|
1165
|
+
issues.push(
|
|
1166
|
+
warning("delivery", "ref.delivery_exists", `Delivery '${task.delivery}' does not match a known delivery.`)
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
if (task.assignee) {
|
|
1170
|
+
const knownAssignee = context.assignees && context.assignees.has(task.assignee) || context.agents && context.agents.has(task.assignee);
|
|
1171
|
+
if (!knownAssignee) {
|
|
1172
|
+
issues.push(
|
|
1173
|
+
warning(
|
|
1174
|
+
"assignee",
|
|
1175
|
+
"ref.assignee_known",
|
|
1176
|
+
`Assignee '${task.assignee}' does not match a known member or agent.`
|
|
1177
|
+
)
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (task.execution?.agent && context.agents && !context.agents.has(task.execution.agent)) {
|
|
1182
|
+
issues.push(
|
|
1183
|
+
warning(
|
|
1184
|
+
"execution.agent",
|
|
1185
|
+
"ref.execution_agent_known",
|
|
1186
|
+
`Execution agent '${task.execution.agent}' does not match a known agent.`
|
|
1187
|
+
)
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
return issues;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/validator/semantic.ts
|
|
1194
|
+
function error3(field, rule, message) {
|
|
1195
|
+
return { level: "error", field, rule, message };
|
|
1196
|
+
}
|
|
1197
|
+
function warning2(field, rule, message) {
|
|
1198
|
+
return { level: "warning", field, rule, message };
|
|
1199
|
+
}
|
|
1200
|
+
function asMap(value) {
|
|
1201
|
+
if (!value) {
|
|
1202
|
+
return /* @__PURE__ */ new Map();
|
|
1203
|
+
}
|
|
1204
|
+
if (value instanceof Map) {
|
|
1205
|
+
return new Map(value);
|
|
1206
|
+
}
|
|
1207
|
+
return new Map(Object.entries(value));
|
|
1208
|
+
}
|
|
1209
|
+
function isNumber(value) {
|
|
1210
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1211
|
+
}
|
|
1212
|
+
function validateSemantic(task, context = {}) {
|
|
1213
|
+
const issues = [];
|
|
1214
|
+
const estimate = task.estimate;
|
|
1215
|
+
if (estimate) {
|
|
1216
|
+
const hasOptimistic = isNumber(estimate.optimistic_hours);
|
|
1217
|
+
const hasExpected = isNumber(estimate.expected_hours);
|
|
1218
|
+
const hasPessimistic = isNumber(estimate.pessimistic_hours);
|
|
1219
|
+
const count = Number(hasOptimistic) + Number(hasExpected) + Number(hasPessimistic);
|
|
1220
|
+
if (count !== 3) {
|
|
1221
|
+
issues.push(
|
|
1222
|
+
error3(
|
|
1223
|
+
"estimate",
|
|
1224
|
+
"sem.estimate_all_three_or_none",
|
|
1225
|
+
"Estimate must include optimistic_hours, expected_hours, and pessimistic_hours together."
|
|
1226
|
+
)
|
|
1227
|
+
);
|
|
1228
|
+
} else if (estimate.optimistic_hours > estimate.expected_hours || estimate.expected_hours > estimate.pessimistic_hours) {
|
|
1229
|
+
issues.push(
|
|
1230
|
+
error3(
|
|
1231
|
+
"estimate",
|
|
1232
|
+
"sem.estimate_order",
|
|
1233
|
+
"Estimate values must satisfy optimistic <= expected <= pessimistic."
|
|
1234
|
+
)
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (task.resources) {
|
|
1239
|
+
const numericFields = [
|
|
1240
|
+
"human_hours",
|
|
1241
|
+
"ai_tokens",
|
|
1242
|
+
"compute_gpu_hours",
|
|
1243
|
+
"cost_usd"
|
|
1244
|
+
];
|
|
1245
|
+
for (const field of numericFields) {
|
|
1246
|
+
const value = task.resources[field];
|
|
1247
|
+
if (value !== void 0 && (!isNumber(value) || value < 0)) {
|
|
1248
|
+
issues.push(
|
|
1249
|
+
error3("resources", "sem.resources_non_negative", `Resource field '${field}' must be non-negative.`)
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const taskStatuses = asMap(context.taskStatuses);
|
|
1255
|
+
if (task.status === "done" && (task.depends_on?.length ?? 0) > 0) {
|
|
1256
|
+
const unresolved = (task.depends_on ?? []).filter((depId) => taskStatuses.get(depId) !== "done");
|
|
1257
|
+
if (unresolved.length > 0) {
|
|
1258
|
+
issues.push(
|
|
1259
|
+
warning2(
|
|
1260
|
+
"status",
|
|
1261
|
+
"sem.done_with_pending_dependencies",
|
|
1262
|
+
`Task is done but has pending dependencies: ${unresolved.join(", ")}.`
|
|
1263
|
+
)
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (task.status === "in_progress" && task.track) {
|
|
1268
|
+
const trackWip = asMap(context.trackWip);
|
|
1269
|
+
const stats = trackWip.get(task.track);
|
|
1270
|
+
if (stats && stats.limit >= 0 && stats.inProgress > stats.limit) {
|
|
1271
|
+
issues.push(
|
|
1272
|
+
warning2(
|
|
1273
|
+
"track",
|
|
1274
|
+
"sem.track_wip_exceeded",
|
|
1275
|
+
`Track '${task.track}' WIP limit exceeded (${stats.inProgress}/${stats.limit}).`
|
|
1276
|
+
)
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const deliveryByTaskId = asMap(context.deliveryByTaskId);
|
|
1281
|
+
const scopedDelivery = deliveryByTaskId.get(task.id);
|
|
1282
|
+
if (scopedDelivery && task.delivery !== scopedDelivery) {
|
|
1283
|
+
issues.push(
|
|
1284
|
+
warning2(
|
|
1285
|
+
"delivery",
|
|
1286
|
+
"sem.delivery_scope_mismatch",
|
|
1287
|
+
`Task is scoped to delivery '${scopedDelivery}' but has delivery='${task.delivery ?? "null"}'.`
|
|
1288
|
+
)
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
if (task.complexity === "unknown") {
|
|
1292
|
+
const linkedSpikeTaskIds = context.linkedSpikeTaskIds ?? /* @__PURE__ */ new Set();
|
|
1293
|
+
if (!linkedSpikeTaskIds.has(task.id)) {
|
|
1294
|
+
issues.push(
|
|
1295
|
+
warning2(
|
|
1296
|
+
"complexity",
|
|
1297
|
+
"sem.complexity_unknown_without_spike",
|
|
1298
|
+
"Task has complexity=unknown without a linked spike task."
|
|
1299
|
+
)
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return issues;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/validator/structural.ts
|
|
1307
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
1308
|
+
var ID_PATTERN = /^[A-Z]+-\d+$/;
|
|
1309
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1310
|
+
function error4(field, rule, message) {
|
|
1311
|
+
return { level: "error", field, rule, message };
|
|
1312
|
+
}
|
|
1313
|
+
function isIsoDate(value) {
|
|
1314
|
+
if (!ISO_DATE_RE.test(value)) {
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
1318
|
+
if (Number.isNaN(date.valueOf())) {
|
|
1319
|
+
return false;
|
|
1320
|
+
}
|
|
1321
|
+
return date.toISOString().slice(0, 10) === value;
|
|
1322
|
+
}
|
|
1323
|
+
function validateStructural(task, context = {}) {
|
|
1324
|
+
const errors = [];
|
|
1325
|
+
const required = ["id", "title", "type", "status", "created", "updated"];
|
|
1326
|
+
for (const field of required) {
|
|
1327
|
+
const value = task[field];
|
|
1328
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1329
|
+
errors.push(
|
|
1330
|
+
error4(String(field), "struct.required_layer_a", `Field '${String(field)}' must be a non-empty string.`)
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (typeof task.id === "string" && task.id.trim().length > 0 && !ID_PATTERN.test(task.id)) {
|
|
1335
|
+
errors.push(error4("id", "struct.id_pattern", `Task id '${task.id}' must match ${ID_PATTERN}.`));
|
|
1336
|
+
}
|
|
1337
|
+
if (typeof task.type === "string" && !Object.values(TaskType).includes(task.type)) {
|
|
1338
|
+
errors.push(error4("type", "struct.type_enum", `Invalid task type '${task.type}'.`));
|
|
1339
|
+
}
|
|
1340
|
+
if (typeof task.status === "string" && !Object.values(TaskStatus).includes(task.status)) {
|
|
1341
|
+
errors.push(error4("status", "struct.status_enum", `Invalid task status '${task.status}'.`));
|
|
1342
|
+
}
|
|
1343
|
+
if (typeof task.created === "string" && !isIsoDate(task.created)) {
|
|
1344
|
+
errors.push(error4("created", "struct.created_iso_date", `Field 'created' must be an ISO date (YYYY-MM-DD).`));
|
|
1345
|
+
}
|
|
1346
|
+
if (typeof task.updated === "string" && !isIsoDate(task.updated)) {
|
|
1347
|
+
errors.push(error4("updated", "struct.updated_iso_date", `Field 'updated' must be an ISO date (YYYY-MM-DD).`));
|
|
1348
|
+
}
|
|
1349
|
+
if (context.filePath) {
|
|
1350
|
+
const expected = import_node_path3.default.basename(context.filePath, import_node_path3.default.extname(context.filePath));
|
|
1351
|
+
if (typeof task.id === "string" && task.id !== expected) {
|
|
1352
|
+
errors.push(
|
|
1353
|
+
error4("id", "struct.id_matches_filename", `Task id '${task.id}' must match filename '${expected}'.`)
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return errors;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/validator/transition.ts
|
|
1361
|
+
var VALID_TRANSITIONS = /* @__PURE__ */ new Map([
|
|
1362
|
+
["todo", /* @__PURE__ */ new Set(["in_progress", "blocked", "canceled"])],
|
|
1363
|
+
["blocked", /* @__PURE__ */ new Set(["todo"])],
|
|
1364
|
+
["in_progress", /* @__PURE__ */ new Set(["in_review", "blocked", "todo", "canceled"])],
|
|
1365
|
+
["in_review", /* @__PURE__ */ new Set(["done", "todo"])],
|
|
1366
|
+
["done", /* @__PURE__ */ new Set()],
|
|
1367
|
+
["canceled", /* @__PURE__ */ new Set(["todo"])]
|
|
1368
|
+
]);
|
|
1369
|
+
function error5(field, rule, message) {
|
|
1370
|
+
return { level: "error", field, rule, message };
|
|
1371
|
+
}
|
|
1372
|
+
function asMap2(value) {
|
|
1373
|
+
if (!value) {
|
|
1374
|
+
return /* @__PURE__ */ new Map();
|
|
1375
|
+
}
|
|
1376
|
+
if (value instanceof Map) {
|
|
1377
|
+
return new Map(value);
|
|
1378
|
+
}
|
|
1379
|
+
return new Map(Object.entries(value));
|
|
1380
|
+
}
|
|
1381
|
+
function validate_transition(fromStatus, toStatus) {
|
|
1382
|
+
const allowed = VALID_TRANSITIONS.get(fromStatus);
|
|
1383
|
+
return Boolean(allowed?.has(toStatus));
|
|
1384
|
+
}
|
|
1385
|
+
function validateTransition(task, toStatus, context = {}) {
|
|
1386
|
+
const issues = [];
|
|
1387
|
+
const fromStatus = task.status;
|
|
1388
|
+
if (!validate_transition(fromStatus, toStatus)) {
|
|
1389
|
+
const allowed = Array.from(VALID_TRANSITIONS.get(fromStatus) ?? []);
|
|
1390
|
+
issues.push(
|
|
1391
|
+
error5(
|
|
1392
|
+
"status",
|
|
1393
|
+
"transition.valid_path",
|
|
1394
|
+
`Invalid transition ${fromStatus} -> ${toStatus}. Allowed: ${allowed.join(", ") || "none"}.`
|
|
1395
|
+
)
|
|
1396
|
+
);
|
|
1397
|
+
return issues;
|
|
1398
|
+
}
|
|
1399
|
+
if (fromStatus === "in_review" && toStatus === "done" && task.governance?.approval_required) {
|
|
1400
|
+
const reviewer = task.governance.reviewer;
|
|
1401
|
+
if (!reviewer || context.actor !== reviewer) {
|
|
1402
|
+
issues.push(
|
|
1403
|
+
error5(
|
|
1404
|
+
"governance.reviewer",
|
|
1405
|
+
"transition.approval_required_reviewer",
|
|
1406
|
+
"Approval-required transition in_review -> done must be performed by the configured reviewer."
|
|
1407
|
+
)
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if (fromStatus === "blocked" && toStatus === "todo") {
|
|
1412
|
+
const dependencyStatuses = asMap2(context.dependencyStatuses);
|
|
1413
|
+
const unresolved = (task.depends_on ?? []).filter((depId) => dependencyStatuses.get(depId) !== "done");
|
|
1414
|
+
if (unresolved.length > 0) {
|
|
1415
|
+
issues.push(
|
|
1416
|
+
error5(
|
|
1417
|
+
"depends_on",
|
|
1418
|
+
"transition.blocked_to_todo_dependencies_done",
|
|
1419
|
+
`blocked -> todo requires all dependencies done. Pending: ${unresolved.join(", ")}.`
|
|
1420
|
+
)
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return issues;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/validator/index.ts
|
|
1428
|
+
function validate(task, context = {}) {
|
|
1429
|
+
const issues = [
|
|
1430
|
+
...validateStructural(task, context),
|
|
1431
|
+
...validateReferential(task, context),
|
|
1432
|
+
...validateSemantic(task, context)
|
|
1433
|
+
];
|
|
1434
|
+
const errors = issues.filter((issue) => issue.level === "error");
|
|
1435
|
+
const warnings = issues.filter((issue) => issue.level === "warning");
|
|
1436
|
+
return {
|
|
1437
|
+
valid: errors.length === 0,
|
|
1438
|
+
errors,
|
|
1439
|
+
warnings,
|
|
1440
|
+
issues
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/core.ts
|
|
1445
|
+
var import_node_fs9 = __toESM(require("fs"), 1);
|
|
1446
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1447
|
+
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
1448
|
+
|
|
1449
|
+
// src/types.ts
|
|
1450
|
+
var ITEM_TYPES = ["epic", "story", "task", "bug", "spike"];
|
|
1451
|
+
var ITEM_STATUSES = ["todo", "in_progress", "blocked", "done"];
|
|
1452
|
+
|
|
1453
|
+
// src/core.ts
|
|
1454
|
+
var ITEM_ID_RE = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)+(?:\.\d+)?$/;
|
|
1455
|
+
var ITEM_DIRS = {
|
|
1456
|
+
epic: "epics",
|
|
1457
|
+
story: "stories",
|
|
1458
|
+
task: "tasks",
|
|
1459
|
+
bug: "bugs",
|
|
1460
|
+
spike: "spikes"
|
|
1461
|
+
};
|
|
1462
|
+
var COOP_DIR = ".coop";
|
|
1463
|
+
var DEFAULT_CONFIG = {
|
|
1464
|
+
spec_version: 1,
|
|
1465
|
+
id_prefix: "COOP",
|
|
1466
|
+
id_strategy: "text"
|
|
1467
|
+
};
|
|
1468
|
+
function toIdKey(value) {
|
|
1469
|
+
return value.trim().toUpperCase();
|
|
1470
|
+
}
|
|
1471
|
+
function repoRootByPackage(cwd) {
|
|
1472
|
+
let current = import_node_path4.default.resolve(cwd);
|
|
1473
|
+
let lastWorkspaceRoot = null;
|
|
1474
|
+
while (true) {
|
|
1475
|
+
const packageJson = import_node_path4.default.join(current, "package.json");
|
|
1476
|
+
const workspaceYaml = import_node_path4.default.join(current, "pnpm-workspace.yaml");
|
|
1477
|
+
if (import_node_fs9.default.existsSync(packageJson) && import_node_fs9.default.existsSync(workspaceYaml)) {
|
|
1478
|
+
lastWorkspaceRoot = current;
|
|
1479
|
+
const hasCoop = import_node_fs9.default.existsSync(import_node_path4.default.join(current, COOP_DIR, "config.yml"));
|
|
1480
|
+
if (hasCoop) return current;
|
|
1481
|
+
}
|
|
1482
|
+
const parent = import_node_path4.default.dirname(current);
|
|
1483
|
+
if (parent === current) return lastWorkspaceRoot;
|
|
1484
|
+
current = parent;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function findRepoRoot(cwd = process.cwd()) {
|
|
1488
|
+
return repoRootByPackage(cwd);
|
|
1489
|
+
}
|
|
1490
|
+
function configPathFor(rootDir, workspaceDir) {
|
|
1491
|
+
return import_node_path4.default.join(rootDir, workspaceDir, "config.yml");
|
|
1492
|
+
}
|
|
1493
|
+
function backlogPathFor(rootDir, workspaceDir) {
|
|
1494
|
+
return import_node_path4.default.join(rootDir, workspaceDir, "backlog");
|
|
1495
|
+
}
|
|
1496
|
+
function releasesPathFor(rootDir, workspaceDir) {
|
|
1497
|
+
return import_node_path4.default.join(rootDir, workspaceDir, "releases");
|
|
1498
|
+
}
|
|
1499
|
+
function detectWorkspaceDir(rootDir) {
|
|
1500
|
+
if (import_node_fs9.default.existsSync(configPathFor(rootDir, COOP_DIR))) return COOP_DIR;
|
|
1501
|
+
if (import_node_fs9.default.existsSync(import_node_path4.default.join(rootDir, COOP_DIR))) return COOP_DIR;
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
function preferredWorkspaceDir(rootDir) {
|
|
1505
|
+
return detectWorkspaceDir(rootDir) ?? COOP_DIR;
|
|
1506
|
+
}
|
|
1507
|
+
function missingConfigError(rootDir) {
|
|
1508
|
+
const coopConfig = import_node_path4.default.relative(rootDir, configPathFor(rootDir, COOP_DIR));
|
|
1509
|
+
return new Error(`COOP config missing at ${coopConfig}. Run: coop init`);
|
|
1510
|
+
}
|
|
1511
|
+
function parseConfig(raw) {
|
|
1512
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).filter((line) => !line.startsWith("#"));
|
|
1513
|
+
const map = /* @__PURE__ */ new Map();
|
|
1514
|
+
for (const line of lines) {
|
|
1515
|
+
const ix = line.indexOf(":");
|
|
1516
|
+
if (ix <= 0) continue;
|
|
1517
|
+
map.set(line.slice(0, ix).trim(), line.slice(ix + 1).trim());
|
|
1518
|
+
}
|
|
1519
|
+
const spec = Number(map.get("spec_version") ?? DEFAULT_CONFIG.spec_version);
|
|
1520
|
+
const idPrefix = map.get("id_prefix") ?? DEFAULT_CONFIG.id_prefix;
|
|
1521
|
+
const strategy = String(map.get("id_strategy") ?? DEFAULT_CONFIG.id_strategy ?? "text").trim().toLowerCase();
|
|
1522
|
+
const rawNextId = map.get("next_id");
|
|
1523
|
+
const nextId = rawNextId != null ? Number(rawNextId) : void 0;
|
|
1524
|
+
if (!Number.isInteger(spec) || spec <= 0) throw new Error("config.yml must define a numeric spec_version.");
|
|
1525
|
+
if (nextId != null && (!Number.isInteger(nextId) || nextId <= 0)) {
|
|
1526
|
+
throw new Error("config.yml next_id must be a positive integer when present.");
|
|
1527
|
+
}
|
|
1528
|
+
if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(idPrefix)) {
|
|
1529
|
+
throw new Error("config.yml must define id_prefix using letters, numbers, or '-'.");
|
|
1530
|
+
}
|
|
1531
|
+
if (strategy !== "text" && strategy !== "counter") {
|
|
1532
|
+
throw new Error("config.yml id_strategy must be either 'text' or 'counter'.");
|
|
1533
|
+
}
|
|
1534
|
+
return {
|
|
1535
|
+
spec_version: spec,
|
|
1536
|
+
id_prefix: idPrefix,
|
|
1537
|
+
id_strategy: strategy,
|
|
1538
|
+
...nextId != null ? { next_id: nextId } : {}
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
function configToString(config) {
|
|
1542
|
+
const lines = [
|
|
1543
|
+
`spec_version: ${config.spec_version}`,
|
|
1544
|
+
`id_prefix: ${config.id_prefix}`,
|
|
1545
|
+
`id_strategy: ${config.id_strategy ?? "text"}`
|
|
1546
|
+
];
|
|
1547
|
+
if (config.next_id != null) {
|
|
1548
|
+
lines.push(`next_id: ${config.next_id}`);
|
|
1549
|
+
}
|
|
1550
|
+
lines.push("");
|
|
1551
|
+
return lines.join("\n");
|
|
1552
|
+
}
|
|
1553
|
+
function toPortablePath(value) {
|
|
1554
|
+
return value.split(import_node_path4.default.sep).join("/");
|
|
1555
|
+
}
|
|
1556
|
+
function ensureReleasesDir(rootDir, workspaceDir) {
|
|
1557
|
+
import_node_fs9.default.mkdirSync(releasesPathFor(rootDir, workspaceDir), { recursive: true });
|
|
1558
|
+
}
|
|
1559
|
+
function releaseHeader(date) {
|
|
1560
|
+
return `## ${date}`;
|
|
1561
|
+
}
|
|
1562
|
+
function releaseEntryLine(item, previousStatus, nextStatus) {
|
|
1563
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1564
|
+
return [
|
|
1565
|
+
`- [${nextStatus.toUpperCase()}] ${item.id} (${item.type})`,
|
|
1566
|
+
` - Title: ${item.title}`,
|
|
1567
|
+
` - Status: ${previousStatus} -> ${nextStatus}`,
|
|
1568
|
+
` - File: ${toPortablePath(item.filePath)}`,
|
|
1569
|
+
` - Updated: ${timestamp}`
|
|
1570
|
+
].join("\n");
|
|
1571
|
+
}
|
|
1572
|
+
function hasReleaseEntry(content, id) {
|
|
1573
|
+
return new RegExp(`^\\- \\[[A-Z_]+\\]\\s+${id}\\b`, "m").test(content);
|
|
1574
|
+
}
|
|
1575
|
+
function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextStatus) {
|
|
1576
|
+
ensureReleasesDir(rootDir, workspaceDir);
|
|
1577
|
+
const now = /* @__PURE__ */ new Date();
|
|
1578
|
+
const date = now.toISOString().slice(0, 10);
|
|
1579
|
+
const releasePath = import_node_path4.default.join(releasesPathFor(rootDir, workspaceDir), `${date}.md`);
|
|
1580
|
+
const heading = "# COOP Release Notes";
|
|
1581
|
+
const dayHeader = releaseHeader(date);
|
|
1582
|
+
const entry = releaseEntryLine(item, previousStatus, nextStatus);
|
|
1583
|
+
if (!import_node_fs9.default.existsSync(releasePath)) {
|
|
1584
|
+
import_node_fs9.default.writeFileSync(
|
|
1585
|
+
releasePath,
|
|
1586
|
+
[
|
|
1587
|
+
`${heading}
|
|
1588
|
+
`,
|
|
1589
|
+
`${dayHeader}
|
|
1590
|
+
`,
|
|
1591
|
+
`${entry}
|
|
1592
|
+
`,
|
|
1593
|
+
""
|
|
1594
|
+
].join("\n"),
|
|
1595
|
+
"utf8"
|
|
1596
|
+
);
|
|
1597
|
+
return toPortablePath(import_node_path4.default.relative(rootDir, releasePath));
|
|
1598
|
+
}
|
|
1599
|
+
const existing = import_node_fs9.default.readFileSync(releasePath, "utf8");
|
|
1600
|
+
if (hasReleaseEntry(existing, item.id)) return toPortablePath(import_node_path4.default.relative(rootDir, releasePath));
|
|
1601
|
+
let nextContent = existing;
|
|
1602
|
+
if (!existing.includes(`## ${date}`)) {
|
|
1603
|
+
if (!nextContent.endsWith("\n")) nextContent += "\n";
|
|
1604
|
+
nextContent += `${dayHeader}
|
|
1605
|
+
`;
|
|
1606
|
+
}
|
|
1607
|
+
if (!nextContent.endsWith("\n")) nextContent += "\n";
|
|
1608
|
+
nextContent += `${entry}
|
|
1609
|
+
`;
|
|
1610
|
+
import_node_fs9.default.writeFileSync(releasePath, `${nextContent}
|
|
1611
|
+
`, "utf8");
|
|
1612
|
+
return toPortablePath(import_node_path4.default.relative(rootDir, releasePath));
|
|
1613
|
+
}
|
|
1614
|
+
function completeItem(rootDir, id) {
|
|
1615
|
+
const state = loadState(rootDir);
|
|
1616
|
+
const existing = getItemById(state, id);
|
|
1617
|
+
if (!existing) throw new Error(`Item ${id} not found.`);
|
|
1618
|
+
if (existing.status === "done") {
|
|
1619
|
+
return {
|
|
1620
|
+
item: existing,
|
|
1621
|
+
previousStatus: existing.status,
|
|
1622
|
+
alreadyComplete: true
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
const updated = updateItem(rootDir, id, { status: "done" });
|
|
1626
|
+
const releaseFile = appendReleaseEntry(rootDir, state.workspaceDir, updated, existing.status, updated.status);
|
|
1627
|
+
return {
|
|
1628
|
+
item: updated,
|
|
1629
|
+
previousStatus: existing.status,
|
|
1630
|
+
releaseFile,
|
|
1631
|
+
alreadyComplete: false
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function normalizePriority(value) {
|
|
1635
|
+
if (typeof value !== "string") return "p2";
|
|
1636
|
+
const trimmed = value.trim().toLowerCase();
|
|
1637
|
+
return trimmed.length ? trimmed : "p2";
|
|
1638
|
+
}
|
|
1639
|
+
function normalizeLabels(value) {
|
|
1640
|
+
if (!value) return [];
|
|
1641
|
+
if (Array.isArray(value)) return value.map((entry) => String(entry).trim()).filter(Boolean);
|
|
1642
|
+
if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
1643
|
+
return [];
|
|
1644
|
+
}
|
|
1645
|
+
function normalizeArray(value) {
|
|
1646
|
+
return normalizeLabels(value);
|
|
1647
|
+
}
|
|
1648
|
+
function normalizeAgent(value) {
|
|
1649
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1650
|
+
const source = value;
|
|
1651
|
+
const out = {};
|
|
1652
|
+
if (typeof source.goal === "string" && source.goal.trim()) out.goal = source.goal.trim();
|
|
1653
|
+
if (Array.isArray(source.context_paths)) out.context_paths = source.context_paths;
|
|
1654
|
+
if (source.commands && typeof source.commands === "object") out.commands = source.commands;
|
|
1655
|
+
if (Array.isArray(source.constraints)) out.constraints = source.constraints;
|
|
1656
|
+
if (Array.isArray(source.risks)) out.risks = source.risks;
|
|
1657
|
+
if (Array.isArray(source.prompt_hints)) out.prompt_hints = source.prompt_hints;
|
|
1658
|
+
return Object.keys(out).length ? out : void 0;
|
|
1659
|
+
}
|
|
1660
|
+
function validateAndNormalize(data, sourceFile) {
|
|
1661
|
+
const id = String(data.id ?? "").trim();
|
|
1662
|
+
const type = String(data.type ?? "").trim();
|
|
1663
|
+
const title = String(data.title ?? "").trim();
|
|
1664
|
+
const status = String(data.status ?? "todo").trim().toLowerCase();
|
|
1665
|
+
const created = String(data.created ?? "").trim() || (/* @__PURE__ */ new Date()).toISOString();
|
|
1666
|
+
const updated = String(data.updated ?? "").trim() || (/* @__PURE__ */ new Date()).toISOString();
|
|
1667
|
+
if (!ITEM_ID_RE.test(id)) throw new Error(`Invalid or missing id in ${sourceFile}`);
|
|
1668
|
+
if (!ITEM_TYPES.includes(type)) throw new Error(`Invalid item type in ${sourceFile}: ${String(data.type)}`);
|
|
1669
|
+
if (!title) throw new Error(`Missing item title in ${sourceFile}`);
|
|
1670
|
+
if (!ITEM_STATUSES.includes(status)) throw new Error(`Invalid status in ${sourceFile}: ${String(data.status)}`);
|
|
1671
|
+
return {
|
|
1672
|
+
id,
|
|
1673
|
+
type,
|
|
1674
|
+
title,
|
|
1675
|
+
status,
|
|
1676
|
+
created,
|
|
1677
|
+
updated,
|
|
1678
|
+
priority: normalizePriority(data.priority),
|
|
1679
|
+
points: typeof data.points === "number" ? data.points : void 0,
|
|
1680
|
+
owner: typeof data.owner === "string" ? data.owner.trim() || void 0 : void 0,
|
|
1681
|
+
labels: normalizeLabels(data.labels),
|
|
1682
|
+
links: data.links ? data.links : void 0,
|
|
1683
|
+
depends_on: normalizeArray(data.depends_on),
|
|
1684
|
+
acceptance: normalizeArray(data.acceptance),
|
|
1685
|
+
agent: normalizeAgent(data.agent),
|
|
1686
|
+
parent_id: typeof data.parent_id === "string" ? data.parent_id.trim() || void 0 : void 0
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
function parseItem(filePath, rootDir) {
|
|
1690
|
+
const raw = import_node_fs9.default.readFileSync(filePath, "utf8");
|
|
1691
|
+
const parsed = (0, import_gray_matter.default)(raw);
|
|
1692
|
+
const data = validateAndNormalize(parsed.data, import_node_path4.default.relative(rootDir, filePath));
|
|
1693
|
+
return {
|
|
1694
|
+
...data,
|
|
1695
|
+
body: parsed.content || "",
|
|
1696
|
+
filePath: import_node_path4.default.relative(rootDir, filePath)
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function walk(dir) {
|
|
1700
|
+
const out = [];
|
|
1701
|
+
if (!import_node_fs9.default.existsSync(dir)) return out;
|
|
1702
|
+
const entries = import_node_fs9.default.readdirSync(dir, { withFileTypes: true });
|
|
1703
|
+
for (const entry of entries) {
|
|
1704
|
+
const file = import_node_path4.default.join(dir, entry.name);
|
|
1705
|
+
if (entry.isDirectory()) out.push(...walk(file));
|
|
1706
|
+
if (entry.isFile() && file.endsWith(".md")) out.push(file);
|
|
1707
|
+
}
|
|
1708
|
+
return out;
|
|
1709
|
+
}
|
|
1710
|
+
function itemPath(type, id, rootDir, workspaceDir) {
|
|
1711
|
+
return import_node_path4.default.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type], `${id}.md`);
|
|
1712
|
+
}
|
|
1713
|
+
function normalizeFrontmatterValue(value) {
|
|
1714
|
+
if (value == null) return void 0;
|
|
1715
|
+
if (Array.isArray(value)) {
|
|
1716
|
+
const normalized = value.map((entry) => typeof entry === "string" ? entry.trim() : entry).filter((entry) => entry != null && entry !== "");
|
|
1717
|
+
return normalized.length ? normalized : void 0;
|
|
1718
|
+
}
|
|
1719
|
+
if (typeof value === "string") {
|
|
1720
|
+
const trimmed = value.trim();
|
|
1721
|
+
return trimmed.length ? trimmed : void 0;
|
|
1722
|
+
}
|
|
1723
|
+
if (typeof value === "object") {
|
|
1724
|
+
const object = value;
|
|
1725
|
+
const out = {};
|
|
1726
|
+
for (const [key, entry] of Object.entries(object)) {
|
|
1727
|
+
const normalized = normalizeFrontmatterValue(entry);
|
|
1728
|
+
if (normalized !== void 0) {
|
|
1729
|
+
out[key] = normalized;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return Object.keys(out).length ? out : void 0;
|
|
1733
|
+
}
|
|
1734
|
+
return value;
|
|
1735
|
+
}
|
|
1736
|
+
function serialize(item, body = "") {
|
|
1737
|
+
const source = {
|
|
1738
|
+
id: item.id,
|
|
1739
|
+
type: item.type,
|
|
1740
|
+
title: item.title,
|
|
1741
|
+
status: item.status,
|
|
1742
|
+
created: item.created,
|
|
1743
|
+
updated: item.updated,
|
|
1744
|
+
priority: item.priority,
|
|
1745
|
+
points: item.points,
|
|
1746
|
+
owner: item.owner,
|
|
1747
|
+
labels: item.labels,
|
|
1748
|
+
links: item.links,
|
|
1749
|
+
depends_on: item.depends_on,
|
|
1750
|
+
acceptance: item.acceptance,
|
|
1751
|
+
agent: item.agent,
|
|
1752
|
+
parent_id: item.parent_id
|
|
1753
|
+
};
|
|
1754
|
+
const frontmatter = {};
|
|
1755
|
+
for (const [key, value] of Object.entries(source)) {
|
|
1756
|
+
const normalized = normalizeFrontmatterValue(value);
|
|
1757
|
+
if (normalized !== void 0) {
|
|
1758
|
+
frontmatter[key] = normalized;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return import_gray_matter.default.stringify(body || "", frontmatter);
|
|
1762
|
+
}
|
|
1763
|
+
function sortPriorityValue(priority) {
|
|
1764
|
+
const value = (priority || "p2").toLowerCase();
|
|
1765
|
+
if (value === "p0") return 0;
|
|
1766
|
+
if (value === "p1") return 1;
|
|
1767
|
+
if (value === "p2") return 2;
|
|
1768
|
+
if (value === "p3") return 3;
|
|
1769
|
+
if (value === "p4") return 4;
|
|
1770
|
+
return 99;
|
|
1771
|
+
}
|
|
1772
|
+
function slugForId(input) {
|
|
1773
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
1774
|
+
}
|
|
1775
|
+
function shortIdToken() {
|
|
1776
|
+
const stamp = Date.now().toString(36);
|
|
1777
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
1778
|
+
return `${stamp}${rand}`.slice(-8);
|
|
1779
|
+
}
|
|
1780
|
+
function nextGeneratedId(config, title, existing) {
|
|
1781
|
+
const strategy = config.id_strategy ?? "text";
|
|
1782
|
+
const prefix = slugForId(config.id_prefix) || "coop";
|
|
1783
|
+
if (strategy === "counter") {
|
|
1784
|
+
const next = config.next_id ?? 1;
|
|
1785
|
+
return `${prefix}-${String(next).padStart(3, "0")}`;
|
|
1786
|
+
}
|
|
1787
|
+
const titleSlug = slugForId(title) || "item";
|
|
1788
|
+
const base = `${prefix}-${titleSlug}`;
|
|
1789
|
+
if (!existing.has(toIdKey(base))) return base;
|
|
1790
|
+
for (let i = 0; i < 64; i += 1) {
|
|
1791
|
+
const candidate = `${base}-${shortIdToken()}`;
|
|
1792
|
+
if (!existing.has(toIdKey(candidate))) return candidate;
|
|
1793
|
+
}
|
|
1794
|
+
throw new Error(`Unable to generate unique id for title "${title}". Provide --id explicitly.`);
|
|
1795
|
+
}
|
|
1796
|
+
function ensureCoopLayout(rootDir) {
|
|
1797
|
+
const workspaceDir = preferredWorkspaceDir(rootDir);
|
|
1798
|
+
const root = import_node_path4.default.join(rootDir, workspaceDir);
|
|
1799
|
+
import_node_fs9.default.mkdirSync(root, { recursive: true });
|
|
1800
|
+
import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "releases"), { recursive: true });
|
|
1801
|
+
import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "plans"), { recursive: true });
|
|
1802
|
+
import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "views"), { recursive: true });
|
|
1803
|
+
import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "templates"), { recursive: true });
|
|
1804
|
+
for (const dir of Object.values(ITEM_DIRS)) {
|
|
1805
|
+
import_node_fs9.default.mkdirSync(import_node_path4.default.join(root, "backlog", dir), { recursive: true });
|
|
1806
|
+
}
|
|
1807
|
+
const configFile = import_node_path4.default.join(root, "config.yml");
|
|
1808
|
+
if (!import_node_fs9.default.existsSync(configFile)) {
|
|
1809
|
+
import_node_fs9.default.writeFileSync(configFile, configToString(DEFAULT_CONFIG), "utf8");
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function loadState(rootDir) {
|
|
1813
|
+
const workspaceDir = detectWorkspaceDir(rootDir);
|
|
1814
|
+
if (!workspaceDir) throw missingConfigError(rootDir);
|
|
1815
|
+
const configPath = configPathFor(rootDir, workspaceDir);
|
|
1816
|
+
if (!import_node_fs9.default.existsSync(configPath)) throw missingConfigError(rootDir);
|
|
1817
|
+
const config = parseConfig(import_node_fs9.default.readFileSync(configPath, "utf8"));
|
|
1818
|
+
const items = [];
|
|
1819
|
+
const itemsById = /* @__PURE__ */ new Map();
|
|
1820
|
+
for (const type of ITEM_TYPES) {
|
|
1821
|
+
const dir = import_node_path4.default.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type]);
|
|
1822
|
+
const files = walk(dir);
|
|
1823
|
+
for (const file of files) {
|
|
1824
|
+
const item = parseItem(file, rootDir);
|
|
1825
|
+
const key = toIdKey(item.id);
|
|
1826
|
+
if (itemsById.has(key)) {
|
|
1827
|
+
throw new Error(`Duplicate item id ${item.id} found while reading ${file}.`);
|
|
1828
|
+
}
|
|
1829
|
+
items.push(item);
|
|
1830
|
+
itemsById.set(key, item);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return { rootDir, workspaceDir, config, items, itemsById };
|
|
1834
|
+
}
|
|
1835
|
+
function queryItems(state, filter) {
|
|
1836
|
+
const status = filter.status?.toLowerCase();
|
|
1837
|
+
const owner = filter.owner?.toLowerCase();
|
|
1838
|
+
const label = filter.label?.toLowerCase();
|
|
1839
|
+
const typeFilter = filter.type;
|
|
1840
|
+
const search = filter.search?.toLowerCase();
|
|
1841
|
+
return state.items.filter((item) => {
|
|
1842
|
+
if (status && item.status !== status) return false;
|
|
1843
|
+
if (owner && (item.owner || "").toLowerCase() !== owner) return false;
|
|
1844
|
+
if (label && !(item.labels ?? []).map((entry) => entry.toLowerCase()).includes(label)) return false;
|
|
1845
|
+
if (typeFilter) {
|
|
1846
|
+
const set = new Set(
|
|
1847
|
+
Array.isArray(typeFilter) ? typeFilter : String(typeFilter).split(",").map((entry) => entry.trim().toLowerCase())
|
|
1848
|
+
);
|
|
1849
|
+
const want = item.type.toLowerCase();
|
|
1850
|
+
if (!set.has(want)) return false;
|
|
1851
|
+
}
|
|
1852
|
+
if (search) {
|
|
1853
|
+
const haystack = `${item.id} ${item.title} ${item.body} ${item.labels?.join(" ")} ${item.owner || ""}`.toLowerCase();
|
|
1854
|
+
if (!haystack.includes(search)) return false;
|
|
1855
|
+
}
|
|
1856
|
+
return true;
|
|
1857
|
+
}).sort((a, b) => {
|
|
1858
|
+
const p = sortPriorityValue(a.priority) - sortPriorityValue(b.priority);
|
|
1859
|
+
if (p !== 0) return p;
|
|
1860
|
+
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
1861
|
+
return a.id.localeCompare(b.id);
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
function getItemById(state, id) {
|
|
1865
|
+
return state.itemsById.get(toIdKey(id)) ?? null;
|
|
1866
|
+
}
|
|
1867
|
+
function createItem(rootDir, params) {
|
|
1868
|
+
const state = loadState(rootDir);
|
|
1869
|
+
const { config } = state;
|
|
1870
|
+
if (!ITEM_TYPES.includes(params.type)) {
|
|
1871
|
+
throw new Error(`Unknown type ${params.type}.`);
|
|
1872
|
+
}
|
|
1873
|
+
if (!params.title?.trim()) {
|
|
1874
|
+
throw new Error("Item title is required.");
|
|
1875
|
+
}
|
|
1876
|
+
const status = params.status || "todo";
|
|
1877
|
+
if (!ITEM_STATUSES.includes(status)) {
|
|
1878
|
+
throw new Error(`Unknown status ${status}.`);
|
|
1879
|
+
}
|
|
1880
|
+
const nextId = params.id ? params.id.trim() : nextGeneratedId(config, params.title, state.itemsById);
|
|
1881
|
+
if (!ITEM_ID_RE.test(nextId)) {
|
|
1882
|
+
throw new Error(`Invalid id ${nextId}.`);
|
|
1883
|
+
}
|
|
1884
|
+
if (state.itemsById.has(toIdKey(nextId))) {
|
|
1885
|
+
throw new Error(`Item ${nextId} already exists.`);
|
|
1886
|
+
}
|
|
1887
|
+
const item = {
|
|
1888
|
+
id: nextId,
|
|
1889
|
+
type: params.type,
|
|
1890
|
+
title: params.title,
|
|
1891
|
+
status,
|
|
1892
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1893
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1894
|
+
priority: normalizePriority(params.priority || "p2"),
|
|
1895
|
+
points: params.points,
|
|
1896
|
+
owner: params.owner,
|
|
1897
|
+
labels: params.labels ?? [],
|
|
1898
|
+
links: params.links,
|
|
1899
|
+
depends_on: params.depends_on ?? [],
|
|
1900
|
+
acceptance: params.acceptance ?? [],
|
|
1901
|
+
agent: params.agent,
|
|
1902
|
+
parent_id: params.parent_id
|
|
1903
|
+
};
|
|
1904
|
+
const itemPathName = itemPath(params.type, item.id, rootDir, state.workspaceDir);
|
|
1905
|
+
import_node_fs9.default.writeFileSync(itemPathName, serialize(item, params.body || ""), "utf8");
|
|
1906
|
+
if ((config.id_strategy ?? "text") === "counter") {
|
|
1907
|
+
const numericMatch = /-(\d+)$/.exec(item.id);
|
|
1908
|
+
if (numericMatch) {
|
|
1909
|
+
const numericValue = Number(numericMatch[1]);
|
|
1910
|
+
if (Number.isInteger(numericValue) && numericValue >= (config.next_id ?? 1)) {
|
|
1911
|
+
config.next_id = numericValue + 1;
|
|
1912
|
+
import_node_fs9.default.writeFileSync(configPathFor(rootDir, state.workspaceDir), configToString(config), "utf8");
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return {
|
|
1917
|
+
...item,
|
|
1918
|
+
body: params.body || "",
|
|
1919
|
+
filePath: import_node_path4.default.relative(rootDir, itemPathName)
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
function updateItem(rootDir, id, patch) {
|
|
1923
|
+
const state = loadState(rootDir);
|
|
1924
|
+
const existing = getItemById(state, id);
|
|
1925
|
+
if (!existing) throw new Error(`Item ${id} not found.`);
|
|
1926
|
+
const next = {
|
|
1927
|
+
...existing,
|
|
1928
|
+
title: patch.title || existing.title,
|
|
1929
|
+
status: patch.status || existing.status,
|
|
1930
|
+
type: patch.type || existing.type,
|
|
1931
|
+
priority: normalizePriority(patch.priority || existing.priority),
|
|
1932
|
+
points: patch.points !== void 0 ? patch.points : existing.points,
|
|
1933
|
+
owner: patch.owner !== void 0 ? patch.owner : existing.owner,
|
|
1934
|
+
labels: patch.labels ?? existing.labels,
|
|
1935
|
+
links: patch.links ?? existing.links,
|
|
1936
|
+
depends_on: patch.depends_on ?? existing.depends_on,
|
|
1937
|
+
acceptance: patch.acceptance ?? existing.acceptance,
|
|
1938
|
+
agent: patch.agent ? patch.agent : existing.agent,
|
|
1939
|
+
created: existing.created,
|
|
1940
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1941
|
+
parent_id: existing.parent_id
|
|
1942
|
+
};
|
|
1943
|
+
if (!ITEM_TYPES.includes(next.type)) throw new Error(`Unknown type ${next.type}.`);
|
|
1944
|
+
if (!ITEM_STATUSES.includes(next.status)) throw new Error(`Unknown status ${next.status}.`);
|
|
1945
|
+
const filePath = import_node_path4.default.join(rootDir, existing.filePath);
|
|
1946
|
+
import_node_fs9.default.writeFileSync(filePath, serialize(next, patch.body || existing.body), "utf8");
|
|
1947
|
+
return {
|
|
1948
|
+
...next,
|
|
1949
|
+
body: patch.body || existing.body,
|
|
1950
|
+
filePath: existing.filePath
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function deleteItem(rootDir, id) {
|
|
1954
|
+
const state = loadState(rootDir);
|
|
1955
|
+
const existing = getItemById(state, id);
|
|
1956
|
+
if (!existing) throw new Error(`Item ${id} not found.`);
|
|
1957
|
+
const existingKey = toIdKey(existing.id);
|
|
1958
|
+
const children = state.items.filter((item) => item.parent_id && toIdKey(item.parent_id) === existingKey);
|
|
1959
|
+
if (children.length > 0) {
|
|
1960
|
+
throw new Error(`Cannot delete ${existing.id} because it has ${children.length} child item(s). Remove children first.`);
|
|
1961
|
+
}
|
|
1962
|
+
import_node_fs9.default.unlinkSync(import_node_path4.default.join(rootDir, existing.filePath));
|
|
1963
|
+
return existing;
|
|
1964
|
+
}
|
|
1965
|
+
function renderAgentPrompt(item) {
|
|
1966
|
+
const lines = [
|
|
1967
|
+
`Task ${item.id}: ${item.title}`,
|
|
1968
|
+
`Type: ${item.type}`,
|
|
1969
|
+
`Priority: ${item.priority || "p2"}`,
|
|
1970
|
+
`Status: ${item.status}`,
|
|
1971
|
+
"",
|
|
1972
|
+
"Acceptance:",
|
|
1973
|
+
...(item.acceptance?.length ? item.acceptance : ["No explicit acceptance criteria"]).map((entry) => `- ${entry}`),
|
|
1974
|
+
"",
|
|
1975
|
+
"Agent context:",
|
|
1976
|
+
...(() => {
|
|
1977
|
+
const agent = item.agent;
|
|
1978
|
+
if (!agent) return ["- No agent data present."];
|
|
1979
|
+
const context = [
|
|
1980
|
+
`- Goal: ${agent.goal || "Not set"}`,
|
|
1981
|
+
`- Context paths: ${(agent.context_paths || []).join(", ") || "None"}`,
|
|
1982
|
+
`- Constraints: ${(agent.constraints || []).join(", ") || "None"}`,
|
|
1983
|
+
`- Risks: ${(agent.risks || []).join(", ") || "None"}`
|
|
1984
|
+
];
|
|
1985
|
+
if (agent.commands) {
|
|
1986
|
+
context.push("- Commands:");
|
|
1987
|
+
for (const [name, command] of Object.entries(agent.commands)) {
|
|
1988
|
+
if (!command) continue;
|
|
1989
|
+
context.push(` - ${name}: ${command}`);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return context;
|
|
1993
|
+
})()
|
|
1994
|
+
];
|
|
1995
|
+
return lines.join("\n");
|
|
1996
|
+
}
|
|
1997
|
+
function validateRepo(rootDir) {
|
|
1998
|
+
const errors = [];
|
|
1999
|
+
const warnings = [];
|
|
2000
|
+
const workspaceDir = detectWorkspaceDir(rootDir);
|
|
2001
|
+
if (!workspaceDir || !import_node_fs9.default.existsSync(configPathFor(rootDir, workspaceDir))) {
|
|
2002
|
+
errors.push("Missing .coop/config.yml. Run coop init first.");
|
|
2003
|
+
return { valid: false, errors, warnings };
|
|
2004
|
+
}
|
|
2005
|
+
let state;
|
|
2006
|
+
try {
|
|
2007
|
+
state = loadState(rootDir);
|
|
2008
|
+
} catch (error6) {
|
|
2009
|
+
errors.push(`Cannot read backlog: ${error6 instanceof Error ? error6.message : String(error6)}`);
|
|
2010
|
+
return { valid: false, errors, warnings };
|
|
2011
|
+
}
|
|
2012
|
+
for (const item of state.items) {
|
|
2013
|
+
if (!ITEM_ID_RE.test(item.id)) errors.push(`Invalid id ${item.id}`);
|
|
2014
|
+
if (!ITEM_TYPES.includes(item.type)) errors.push(`Invalid type ${item.type} in ${item.id}`);
|
|
2015
|
+
if (!ITEM_STATUSES.includes(item.status)) errors.push(`Invalid status ${item.status} in ${item.id}`);
|
|
2016
|
+
if (!item.title.trim()) errors.push(`Missing title in ${item.id}`);
|
|
2017
|
+
for (const dep of item.depends_on ?? []) {
|
|
2018
|
+
if (!state.itemsById.has(toIdKey(dep))) {
|
|
2019
|
+
errors.push(`Item ${item.id} depends_on missing target ${dep}`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (item.parent_id && !state.itemsById.has(toIdKey(item.parent_id))) {
|
|
2023
|
+
warnings.push(`Item ${item.id} has unresolved parent_id ${item.parent_id}`);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
valid: errors.length === 0,
|
|
2028
|
+
errors,
|
|
2029
|
+
warnings
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2033
|
+
0 && (module.exports = {
|
|
2034
|
+
ArtifactType,
|
|
2035
|
+
CURRENT_SCHEMA_VERSION,
|
|
2036
|
+
DeliveryStatus,
|
|
2037
|
+
ExecutorType,
|
|
2038
|
+
ITEM_STATUSES,
|
|
2039
|
+
ITEM_TYPES,
|
|
2040
|
+
IdeaStatus,
|
|
2041
|
+
RiskLevel,
|
|
2042
|
+
RunbookAction,
|
|
2043
|
+
TaskComplexity,
|
|
2044
|
+
TaskDeterminism,
|
|
2045
|
+
TaskPriority,
|
|
2046
|
+
TaskStatus,
|
|
2047
|
+
TaskType,
|
|
2048
|
+
VALID_TASK_TRANSITIONS,
|
|
2049
|
+
VALID_TRANSITIONS,
|
|
2050
|
+
build_graph,
|
|
2051
|
+
check_blocked,
|
|
2052
|
+
check_unblocked,
|
|
2053
|
+
completeItem,
|
|
2054
|
+
compute_all_readiness,
|
|
2055
|
+
compute_readiness,
|
|
2056
|
+
compute_readiness_with_corrections,
|
|
2057
|
+
createItem,
|
|
2058
|
+
deleteItem,
|
|
2059
|
+
detect_cycle,
|
|
2060
|
+
ensureCoopLayout,
|
|
2061
|
+
extract_subgraph,
|
|
2062
|
+
findRepoRoot,
|
|
2063
|
+
find_external_dependencies,
|
|
2064
|
+
getItemById,
|
|
2065
|
+
loadState,
|
|
2066
|
+
load_graph,
|
|
2067
|
+
parseDeliveryContent,
|
|
2068
|
+
parseDeliveryFile,
|
|
2069
|
+
parseFrontmatterContent,
|
|
2070
|
+
parseFrontmatterFile,
|
|
2071
|
+
parseIdeaContent,
|
|
2072
|
+
parseIdeaFile,
|
|
2073
|
+
parseTaskContent,
|
|
2074
|
+
parseTaskFile,
|
|
2075
|
+
parseYamlContent,
|
|
2076
|
+
parseYamlFile,
|
|
2077
|
+
partition_by_readiness,
|
|
2078
|
+
queryItems,
|
|
2079
|
+
read_schema_version,
|
|
2080
|
+
renderAgentPrompt,
|
|
2081
|
+
stringifyFrontmatter,
|
|
2082
|
+
topological_sort,
|
|
2083
|
+
transition,
|
|
2084
|
+
transitive_dependencies,
|
|
2085
|
+
transitive_dependents,
|
|
2086
|
+
updateItem,
|
|
2087
|
+
validate,
|
|
2088
|
+
validateReferential,
|
|
2089
|
+
validateRepo,
|
|
2090
|
+
validateSemantic,
|
|
2091
|
+
validateStructural,
|
|
2092
|
+
validateTransition,
|
|
2093
|
+
validate_graph,
|
|
2094
|
+
validate_transition,
|
|
2095
|
+
writeTask,
|
|
2096
|
+
write_schema_version
|
|
2097
|
+
});
|