@kitsy/coop-core 0.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-UK4JN4TZ.js +951 -0
- package/dist/index.cjs +3866 -240
- package/dist/index.d.cts +832 -14
- package/dist/index.d.ts +832 -14
- package/dist/index.js +2886 -245
- package/dist/planning/monte-carlo-worker.cjs +670 -0
- package/dist/planning/monte-carlo-worker.d.cts +2 -0
- package/dist/planning/monte-carlo-worker.d.ts +2 -0
- package/dist/planning/monte-carlo-worker.js +14 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -31,13 +31,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ArtifactType: () => ArtifactType,
|
|
34
|
+
COOP_EVENT_TYPES: () => COOP_EVENT_TYPES,
|
|
34
35
|
CURRENT_SCHEMA_VERSION: () => CURRENT_SCHEMA_VERSION,
|
|
36
|
+
CoopEventEmitter: () => CoopEventEmitter,
|
|
37
|
+
DEFAULT_SCORE_WEIGHTS: () => DEFAULT_SCORE_WEIGHTS,
|
|
35
38
|
DeliveryStatus: () => DeliveryStatus,
|
|
36
39
|
ExecutorType: () => ExecutorType,
|
|
37
40
|
ITEM_STATUSES: () => ITEM_STATUSES,
|
|
38
41
|
ITEM_TYPES: () => ITEM_TYPES,
|
|
39
42
|
IdeaStatus: () => IdeaStatus,
|
|
43
|
+
IndexManager: () => IndexManager,
|
|
44
|
+
MIGRATIONS: () => MIGRATIONS,
|
|
40
45
|
RiskLevel: () => RiskLevel,
|
|
46
|
+
RunStatus: () => RunStatus,
|
|
47
|
+
RunStepStatus: () => RunStepStatus,
|
|
41
48
|
RunbookAction: () => RunbookAction,
|
|
42
49
|
TaskComplexity: () => TaskComplexity,
|
|
43
50
|
TaskDeterminism: () => TaskDeterminism,
|
|
@@ -46,23 +53,63 @@ __export(index_exports, {
|
|
|
46
53
|
TaskType: () => TaskType,
|
|
47
54
|
VALID_TASK_TRANSITIONS: () => VALID_TASK_TRANSITIONS,
|
|
48
55
|
VALID_TRANSITIONS: () => VALID_TRANSITIONS,
|
|
56
|
+
allocate: () => allocate,
|
|
57
|
+
allocate_ai: () => allocate_ai,
|
|
58
|
+
allocate_ai_tokens: () => allocate_ai_tokens,
|
|
59
|
+
analyze_feasibility: () => analyze_feasibility,
|
|
60
|
+
analyze_what_if: () => analyze_what_if,
|
|
61
|
+
build_capacity_ledger: () => build_capacity_ledger,
|
|
49
62
|
build_graph: () => build_graph,
|
|
50
63
|
check_blocked: () => check_blocked,
|
|
64
|
+
check_permission: () => check_permission,
|
|
51
65
|
check_unblocked: () => check_unblocked,
|
|
66
|
+
check_wip: () => check_wip,
|
|
52
67
|
completeItem: () => completeItem,
|
|
68
|
+
complexity_penalty: () => complexity_penalty,
|
|
53
69
|
compute_all_readiness: () => compute_all_readiness,
|
|
70
|
+
compute_critical_path: () => compute_critical_path,
|
|
54
71
|
compute_readiness: () => compute_readiness,
|
|
55
72
|
compute_readiness_with_corrections: () => compute_readiness_with_corrections,
|
|
73
|
+
compute_score: () => compute_score,
|
|
74
|
+
compute_velocity: () => compute_velocity,
|
|
75
|
+
coop_project_config_path: () => coop_project_config_path,
|
|
76
|
+
coop_project_root: () => coop_project_root,
|
|
77
|
+
coop_projects_dir: () => coop_projects_dir,
|
|
78
|
+
coop_workspace_config_path: () => coop_workspace_config_path,
|
|
79
|
+
coop_workspace_dir: () => coop_workspace_dir,
|
|
56
80
|
createItem: () => createItem,
|
|
81
|
+
create_seeded_rng: () => create_seeded_rng,
|
|
82
|
+
critical_path_weight: () => critical_path_weight,
|
|
57
83
|
deleteItem: () => deleteItem,
|
|
84
|
+
dependency_unlock_weight: () => dependency_unlock_weight,
|
|
58
85
|
detect_cycle: () => detect_cycle,
|
|
86
|
+
detect_delivery_risks: () => detect_delivery_risks,
|
|
87
|
+
determinism_weight: () => determinism_weight,
|
|
88
|
+
effective_weekly_hours: () => effective_weekly_hours,
|
|
89
|
+
effort_or_default: () => effort_or_default,
|
|
59
90
|
ensureCoopLayout: () => ensureCoopLayout,
|
|
91
|
+
ensure_workspace_layout: () => ensure_workspace_layout,
|
|
92
|
+
executor_fit_weight: () => executor_fit_weight,
|
|
93
|
+
external_dependencies_for_task: () => external_dependencies_for_task,
|
|
60
94
|
extract_subgraph: () => extract_subgraph,
|
|
61
95
|
findRepoRoot: () => findRepoRoot,
|
|
62
96
|
find_external_dependencies: () => find_external_dependencies,
|
|
63
97
|
getItemById: () => getItemById,
|
|
98
|
+
get_remaining_tokens: () => get_remaining_tokens,
|
|
99
|
+
get_user_role: () => get_user_role,
|
|
100
|
+
has_legacy_project_layout: () => has_legacy_project_layout,
|
|
101
|
+
has_v2_projects_layout: () => has_v2_projects_layout,
|
|
102
|
+
is_external_dependency: () => is_external_dependency,
|
|
103
|
+
is_project_initialized: () => is_project_initialized,
|
|
104
|
+
list_projects: () => list_projects,
|
|
64
105
|
loadState: () => loadState,
|
|
106
|
+
load_auth_config: () => load_auth_config,
|
|
107
|
+
load_completed_runs: () => load_completed_runs,
|
|
65
108
|
load_graph: () => load_graph,
|
|
109
|
+
load_plugins: () => load_plugins,
|
|
110
|
+
migrate_repository: () => migrate_repository,
|
|
111
|
+
migrate_task: () => migrate_task,
|
|
112
|
+
monte_carlo_forecast: () => monte_carlo_forecast,
|
|
66
113
|
parseDeliveryContent: () => parseDeliveryContent,
|
|
67
114
|
parseDeliveryFile: () => parseDeliveryFile,
|
|
68
115
|
parseFrontmatterContent: () => parseFrontmatterContent,
|
|
@@ -73,16 +120,38 @@ __export(index_exports, {
|
|
|
73
120
|
parseTaskFile: () => parseTaskFile,
|
|
74
121
|
parseYamlContent: () => parseYamlContent,
|
|
75
122
|
parseYamlFile: () => parseYamlFile,
|
|
123
|
+
parse_external_dependency: () => parse_external_dependency,
|
|
76
124
|
partition_by_readiness: () => partition_by_readiness,
|
|
125
|
+
pert_hours: () => pert_hours,
|
|
126
|
+
pert_stddev: () => pert_stddev,
|
|
127
|
+
priority_weight: () => priority_weight,
|
|
77
128
|
queryItems: () => queryItems,
|
|
129
|
+
read_project_config: () => read_project_config,
|
|
78
130
|
read_schema_version: () => read_schema_version,
|
|
131
|
+
read_workspace_config: () => read_workspace_config,
|
|
79
132
|
renderAgentPrompt: () => renderAgentPrompt,
|
|
133
|
+
repo_default_project_id: () => repo_default_project_id,
|
|
134
|
+
repo_default_project_name: () => repo_default_project_name,
|
|
135
|
+
resolve_external_dependencies: () => resolve_external_dependencies,
|
|
136
|
+
resolve_project: () => resolve_project,
|
|
137
|
+
risk_penalty: () => risk_penalty,
|
|
138
|
+
run_hook: () => run_hook,
|
|
139
|
+
run_monte_carlo_chunk: () => run_monte_carlo_chunk,
|
|
140
|
+
run_plugins_for_event: () => run_plugins_for_event,
|
|
141
|
+
sample_pert_beta: () => sample_pert_beta,
|
|
142
|
+
sample_task_hours: () => sample_task_hours,
|
|
143
|
+
schedule_next: () => schedule_next,
|
|
144
|
+
simulate_schedule: () => simulate_schedule,
|
|
80
145
|
stringifyFrontmatter: () => stringifyFrontmatter,
|
|
146
|
+
stringifyYamlContent: () => stringifyYamlContent,
|
|
147
|
+
task_effort_hours: () => task_effort_hours,
|
|
81
148
|
topological_sort: () => topological_sort,
|
|
82
149
|
transition: () => transition,
|
|
83
150
|
transitive_dependencies: () => transitive_dependencies,
|
|
84
151
|
transitive_dependents: () => transitive_dependents,
|
|
152
|
+
type_weight: () => type_weight,
|
|
85
153
|
updateItem: () => updateItem,
|
|
154
|
+
urgency_weight: () => urgency_weight,
|
|
86
155
|
validate: () => validate,
|
|
87
156
|
validateReferential: () => validateReferential,
|
|
88
157
|
validateRepo: () => validateRepo,
|
|
@@ -92,10 +161,166 @@ __export(index_exports, {
|
|
|
92
161
|
validate_graph: () => validate_graph,
|
|
93
162
|
validate_transition: () => validate_transition,
|
|
94
163
|
writeTask: () => writeTask,
|
|
95
|
-
|
|
164
|
+
writeYamlFile: () => writeYamlFile,
|
|
165
|
+
write_schema_version: () => write_schema_version,
|
|
166
|
+
write_workspace_config: () => write_workspace_config
|
|
96
167
|
});
|
|
97
168
|
module.exports = __toCommonJS(index_exports);
|
|
98
169
|
|
|
170
|
+
// src/planning/estimation.ts
|
|
171
|
+
var COMPLEXITY_FALLBACK_HOURS = {
|
|
172
|
+
trivial: 1,
|
|
173
|
+
small: 4,
|
|
174
|
+
medium: 12,
|
|
175
|
+
large: 32,
|
|
176
|
+
unknown: Number.NaN
|
|
177
|
+
};
|
|
178
|
+
function validateEstimateValue(value, field) {
|
|
179
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
180
|
+
throw new Error(`estimate.${field} must be a positive number.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function complexity_fallback_hours(complexity) {
|
|
184
|
+
if (!complexity) {
|
|
185
|
+
throw new Error("Task effort cannot be computed: missing complexity and estimate.");
|
|
186
|
+
}
|
|
187
|
+
if (complexity === "unknown") {
|
|
188
|
+
throw new Error("Task effort cannot be computed: complexity is 'unknown' and estimate is missing.");
|
|
189
|
+
}
|
|
190
|
+
return COMPLEXITY_FALLBACK_HOURS[complexity];
|
|
191
|
+
}
|
|
192
|
+
function pert_hours(estimate) {
|
|
193
|
+
validateEstimateValue(estimate.optimistic_hours, "optimistic_hours");
|
|
194
|
+
validateEstimateValue(estimate.expected_hours, "expected_hours");
|
|
195
|
+
validateEstimateValue(estimate.pessimistic_hours, "pessimistic_hours");
|
|
196
|
+
return (estimate.optimistic_hours + 4 * estimate.expected_hours + estimate.pessimistic_hours) / 6;
|
|
197
|
+
}
|
|
198
|
+
function pert_stddev(estimate) {
|
|
199
|
+
validateEstimateValue(estimate.optimistic_hours, "optimistic_hours");
|
|
200
|
+
validateEstimateValue(estimate.pessimistic_hours, "pessimistic_hours");
|
|
201
|
+
return (estimate.pessimistic_hours - estimate.optimistic_hours) / 6;
|
|
202
|
+
}
|
|
203
|
+
function task_effort_hours(task) {
|
|
204
|
+
const humanHours = task.resources?.human_hours;
|
|
205
|
+
if (typeof humanHours === "number" && Number.isFinite(humanHours)) {
|
|
206
|
+
if (humanHours < 0) {
|
|
207
|
+
throw new Error("resources.human_hours must be >= 0.");
|
|
208
|
+
}
|
|
209
|
+
return humanHours;
|
|
210
|
+
}
|
|
211
|
+
if (task.estimate) {
|
|
212
|
+
return pert_hours(task.estimate);
|
|
213
|
+
}
|
|
214
|
+
return complexity_fallback_hours(task.complexity);
|
|
215
|
+
}
|
|
216
|
+
function effort_or_default(task, config) {
|
|
217
|
+
const humanHours = task.resources?.human_hours;
|
|
218
|
+
if (typeof humanHours === "number" && Number.isFinite(humanHours)) {
|
|
219
|
+
if (humanHours < 0) {
|
|
220
|
+
throw new Error("resources.human_hours must be >= 0.");
|
|
221
|
+
}
|
|
222
|
+
return humanHours;
|
|
223
|
+
}
|
|
224
|
+
if (task.estimate) {
|
|
225
|
+
return pert_hours(task.estimate);
|
|
226
|
+
}
|
|
227
|
+
const defaultComplexity = config.defaults?.task?.complexity;
|
|
228
|
+
return complexity_fallback_hours(task.complexity ?? defaultComplexity);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/graph/external.ts
|
|
232
|
+
var EXTERNAL_PREFIX = "external:";
|
|
233
|
+
function is_external_dependency(value) {
|
|
234
|
+
return value.trim().toLowerCase().startsWith(EXTERNAL_PREFIX);
|
|
235
|
+
}
|
|
236
|
+
function parse_external_dependency(value) {
|
|
237
|
+
const normalized = value.trim();
|
|
238
|
+
if (!is_external_dependency(normalized)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const rest = normalized.slice(EXTERNAL_PREFIX.length);
|
|
242
|
+
const separator = rest.indexOf("/");
|
|
243
|
+
if (separator <= 0 || separator === rest.length - 1) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const repo = rest.slice(0, separator).trim();
|
|
247
|
+
const taskId = rest.slice(separator + 1).trim();
|
|
248
|
+
if (!repo || !taskId) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
raw: normalized,
|
|
253
|
+
repo,
|
|
254
|
+
task_id: taskId
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function external_dependencies_for_task(graph, taskId) {
|
|
258
|
+
return [...graph.external_dependencies?.get(taskId) ?? []];
|
|
259
|
+
}
|
|
260
|
+
function joinUrl(baseUrl, pathName) {
|
|
261
|
+
return `${baseUrl.replace(/\/+$/g, "")}${pathName.startsWith("/") ? pathName : `/${pathName}`}`;
|
|
262
|
+
}
|
|
263
|
+
function resolvedStatus(taskStatus) {
|
|
264
|
+
if (!taskStatus) return "unknown";
|
|
265
|
+
if (taskStatus === "done" || taskStatus === "canceled") return "resolved";
|
|
266
|
+
return "blocked";
|
|
267
|
+
}
|
|
268
|
+
async function resolve_external_dependencies(graph, options) {
|
|
269
|
+
const fetchImpl = options.fetch_impl ?? fetch;
|
|
270
|
+
const resolutions = /* @__PURE__ */ new Map();
|
|
271
|
+
const externalMap = graph.external_dependencies ?? /* @__PURE__ */ new Map();
|
|
272
|
+
for (const [taskId, deps] of externalMap.entries()) {
|
|
273
|
+
const results = [];
|
|
274
|
+
for (const dep of deps) {
|
|
275
|
+
const remote = options.remotes[dep.repo];
|
|
276
|
+
if (!remote?.base_url) {
|
|
277
|
+
results.push({
|
|
278
|
+
dependency: dep,
|
|
279
|
+
status: "unknown",
|
|
280
|
+
warning: `No API base URL configured for external repo '${dep.repo}'.`
|
|
281
|
+
});
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetchImpl(joinUrl(remote.base_url, `/api/tasks/${encodeURIComponent(dep.task_id)}`));
|
|
286
|
+
if (response.status === 404) {
|
|
287
|
+
results.push({
|
|
288
|
+
dependency: dep,
|
|
289
|
+
status: "missing",
|
|
290
|
+
warning: `External task '${dep.raw}' was not found at ${remote.base_url}.`
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (!response.ok) {
|
|
295
|
+
results.push({
|
|
296
|
+
dependency: dep,
|
|
297
|
+
status: "unreachable",
|
|
298
|
+
warning: `External task '${dep.raw}' returned HTTP ${response.status}.`
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const payload = await response.json();
|
|
303
|
+
const taskRecord = payload && typeof payload === "object" && payload.task && typeof payload.task === "object" ? payload.task : payload;
|
|
304
|
+
const taskStatus = typeof taskRecord.status === "string" ? taskRecord.status : null;
|
|
305
|
+
results.push({
|
|
306
|
+
dependency: dep,
|
|
307
|
+
status: resolvedStatus(taskStatus),
|
|
308
|
+
task_status: taskStatus,
|
|
309
|
+
warning: resolvedStatus(taskStatus) === "resolved" ? void 0 : `External task '${dep.raw}' is ${taskStatus ?? "unknown"}.`
|
|
310
|
+
});
|
|
311
|
+
} catch {
|
|
312
|
+
results.push({
|
|
313
|
+
dependency: dep,
|
|
314
|
+
status: "unreachable",
|
|
315
|
+
warning: `External task '${dep.raw}' could not be reached.`
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
resolutions.set(taskId, results);
|
|
320
|
+
}
|
|
321
|
+
return resolutions;
|
|
322
|
+
}
|
|
323
|
+
|
|
99
324
|
// src/graph/dag.ts
|
|
100
325
|
function existingDeps(graph, nodeId) {
|
|
101
326
|
const deps = graph.forward.get(nodeId) ?? /* @__PURE__ */ new Set();
|
|
@@ -216,13 +441,25 @@ function build_graph(tasks, context = {}) {
|
|
|
216
441
|
}
|
|
217
442
|
const forward = /* @__PURE__ */ new Map();
|
|
218
443
|
const reverse = /* @__PURE__ */ new Map();
|
|
444
|
+
const externalDependencies = /* @__PURE__ */ new Map();
|
|
219
445
|
for (const nodeId of nodes.keys()) {
|
|
220
446
|
forward.set(nodeId, /* @__PURE__ */ new Set());
|
|
221
447
|
reverse.set(nodeId, /* @__PURE__ */ new Set());
|
|
448
|
+
externalDependencies.set(nodeId, []);
|
|
222
449
|
}
|
|
223
450
|
for (const task of tasks) {
|
|
224
|
-
const deps = new Set(
|
|
451
|
+
const deps = /* @__PURE__ */ new Set();
|
|
452
|
+
const externalDeps = [];
|
|
453
|
+
for (const depId of task.depends_on ?? []) {
|
|
454
|
+
const external = parse_external_dependency(depId);
|
|
455
|
+
if (external) {
|
|
456
|
+
externalDeps.push(external);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
deps.add(depId);
|
|
460
|
+
}
|
|
225
461
|
forward.set(task.id, deps);
|
|
462
|
+
externalDependencies.set(task.id, externalDeps);
|
|
226
463
|
for (const depId of deps) {
|
|
227
464
|
if (!nodes.has(depId)) continue;
|
|
228
465
|
reverse.get(depId)?.add(task.id);
|
|
@@ -235,16 +472,266 @@ function build_graph(tasks, context = {}) {
|
|
|
235
472
|
topological_order: [],
|
|
236
473
|
tracks: context.tracks ? new Map(context.tracks) : /* @__PURE__ */ new Map(),
|
|
237
474
|
resources: context.resources ? new Map(context.resources) : /* @__PURE__ */ new Map(),
|
|
238
|
-
deliveries: context.deliveries ? new Map(context.deliveries) : /* @__PURE__ */ new Map()
|
|
475
|
+
deliveries: context.deliveries ? new Map(context.deliveries) : /* @__PURE__ */ new Map(),
|
|
476
|
+
external_dependencies: externalDependencies
|
|
239
477
|
};
|
|
240
478
|
graph.topological_order = topological_sort(graph);
|
|
241
479
|
return graph;
|
|
242
480
|
}
|
|
243
481
|
|
|
244
|
-
// src/graph/
|
|
482
|
+
// src/graph/subgraph.ts
|
|
483
|
+
function extract_subgraph(graph, taskIds) {
|
|
484
|
+
const selected = new Set(taskIds.filter((taskId) => graph.nodes.has(taskId)));
|
|
485
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
486
|
+
for (const taskId of selected) {
|
|
487
|
+
const task = graph.nodes.get(taskId);
|
|
488
|
+
if (task) nodes.set(taskId, task);
|
|
489
|
+
}
|
|
490
|
+
const forward = /* @__PURE__ */ new Map();
|
|
491
|
+
const reverse = /* @__PURE__ */ new Map();
|
|
492
|
+
for (const taskId of selected) {
|
|
493
|
+
forward.set(taskId, /* @__PURE__ */ new Set());
|
|
494
|
+
reverse.set(taskId, /* @__PURE__ */ new Set());
|
|
495
|
+
}
|
|
496
|
+
for (const taskId of selected) {
|
|
497
|
+
const deps = graph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
498
|
+
const internalDeps = new Set(Array.from(deps).filter((depId) => selected.has(depId)));
|
|
499
|
+
forward.set(taskId, internalDeps);
|
|
500
|
+
for (const depId of internalDeps) {
|
|
501
|
+
reverse.get(depId)?.add(taskId);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const subgraph = {
|
|
505
|
+
nodes,
|
|
506
|
+
forward,
|
|
507
|
+
reverse,
|
|
508
|
+
topological_order: [],
|
|
509
|
+
tracks: new Map(graph.tracks),
|
|
510
|
+
resources: new Map(graph.resources),
|
|
511
|
+
deliveries: new Map(graph.deliveries),
|
|
512
|
+
external_dependencies: new Map(
|
|
513
|
+
Array.from(selected).map((taskId) => [taskId, external_dependencies_for_task(graph, taskId)])
|
|
514
|
+
)
|
|
515
|
+
};
|
|
516
|
+
subgraph.topological_order = topological_sort(subgraph);
|
|
517
|
+
return subgraph;
|
|
518
|
+
}
|
|
519
|
+
function find_external_dependencies(subgraph, fullGraph) {
|
|
520
|
+
const external = /* @__PURE__ */ new Set();
|
|
521
|
+
for (const taskId of subgraph.nodes.keys()) {
|
|
522
|
+
const deps = fullGraph.forward.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
523
|
+
for (const depId of deps) {
|
|
524
|
+
if (!subgraph.nodes.has(depId) && fullGraph.nodes.has(depId)) {
|
|
525
|
+
external.add(depId);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const dep of external_dependencies_for_task(fullGraph, taskId)) {
|
|
529
|
+
external.add(dep.raw);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return Array.from(external).sort((a, b) => a.localeCompare(b));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/graph/critical-path.ts
|
|
536
|
+
var DEFAULT_CONFIG = {
|
|
537
|
+
version: 2,
|
|
538
|
+
project: {
|
|
539
|
+
name: "COOP",
|
|
540
|
+
id: "coop"
|
|
541
|
+
},
|
|
542
|
+
defaults: {
|
|
543
|
+
task: {
|
|
544
|
+
complexity: "medium"
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
var EPSILON = 1e-9;
|
|
549
|
+
function durationHours(task) {
|
|
550
|
+
return effort_or_default(task, DEFAULT_CONFIG);
|
|
551
|
+
}
|
|
552
|
+
function rootsOf(graph) {
|
|
553
|
+
const roots = [];
|
|
554
|
+
for (const id of graph.nodes.keys()) {
|
|
555
|
+
if ((graph.forward.get(id)?.size ?? 0) === 0) {
|
|
556
|
+
roots.push(id);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return roots;
|
|
560
|
+
}
|
|
561
|
+
function leavesOf(graph) {
|
|
562
|
+
const leaves = [];
|
|
563
|
+
for (const id of graph.nodes.keys()) {
|
|
564
|
+
if ((graph.reverse.get(id)?.size ?? 0) === 0) {
|
|
565
|
+
leaves.push(id);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return leaves;
|
|
569
|
+
}
|
|
570
|
+
function compute_critical_path(delivery, graph) {
|
|
571
|
+
const include = new Set(delivery.scope.include ?? []);
|
|
572
|
+
for (const excluded of delivery.scope.exclude ?? []) {
|
|
573
|
+
include.delete(excluded);
|
|
574
|
+
}
|
|
575
|
+
const subgraph = extract_subgraph(graph, [...include]);
|
|
576
|
+
if (subgraph.nodes.size === 0) {
|
|
577
|
+
return {
|
|
578
|
+
critical_path: [],
|
|
579
|
+
project_duration_hours: 0,
|
|
580
|
+
per_task: []
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const roots = new Set(rootsOf(subgraph));
|
|
584
|
+
const leaves = new Set(leavesOf(subgraph));
|
|
585
|
+
const es = /* @__PURE__ */ new Map();
|
|
586
|
+
const ef = /* @__PURE__ */ new Map();
|
|
587
|
+
const ls = /* @__PURE__ */ new Map();
|
|
588
|
+
const lf = /* @__PURE__ */ new Map();
|
|
589
|
+
const durations = /* @__PURE__ */ new Map();
|
|
590
|
+
for (const taskId of subgraph.topological_order) {
|
|
591
|
+
const task = subgraph.nodes.get(taskId);
|
|
592
|
+
if (!task) continue;
|
|
593
|
+
const duration = durationHours(task);
|
|
594
|
+
durations.set(taskId, duration);
|
|
595
|
+
let earliestStart = 0;
|
|
596
|
+
if (!roots.has(taskId)) {
|
|
597
|
+
const deps = [...subgraph.forward.get(taskId) ?? /* @__PURE__ */ new Set()];
|
|
598
|
+
earliestStart = deps.length === 0 ? 0 : Math.max(...deps.map((depId) => ef.get(depId) ?? 0));
|
|
599
|
+
}
|
|
600
|
+
es.set(taskId, earliestStart);
|
|
601
|
+
ef.set(taskId, earliestStart + duration);
|
|
602
|
+
}
|
|
603
|
+
const project_duration_hours = Math.max(...[...leaves].map((id) => ef.get(id) ?? 0));
|
|
604
|
+
const reverseTopo = [...subgraph.topological_order].reverse();
|
|
605
|
+
for (const taskId of reverseTopo) {
|
|
606
|
+
const duration = durations.get(taskId) ?? 0;
|
|
607
|
+
let latestFinish = project_duration_hours;
|
|
608
|
+
if (!leaves.has(taskId)) {
|
|
609
|
+
const successors = [...subgraph.reverse.get(taskId) ?? /* @__PURE__ */ new Set()];
|
|
610
|
+
latestFinish = successors.length === 0 ? project_duration_hours : Math.min(...successors.map((id) => ls.get(id) ?? project_duration_hours));
|
|
611
|
+
}
|
|
612
|
+
lf.set(taskId, latestFinish);
|
|
613
|
+
ls.set(taskId, latestFinish - duration);
|
|
614
|
+
}
|
|
615
|
+
const per_task = [];
|
|
616
|
+
for (const id of subgraph.nodes.keys()) {
|
|
617
|
+
const ES = es.get(id) ?? 0;
|
|
618
|
+
const EF = ef.get(id) ?? 0;
|
|
619
|
+
const LS = ls.get(id) ?? 0;
|
|
620
|
+
const LF = lf.get(id) ?? 0;
|
|
621
|
+
const slack = LS - ES;
|
|
622
|
+
per_task.push({
|
|
623
|
+
id,
|
|
624
|
+
ES,
|
|
625
|
+
EF,
|
|
626
|
+
LS,
|
|
627
|
+
LF,
|
|
628
|
+
slack,
|
|
629
|
+
on_critical_path: Math.abs(slack) <= EPSILON
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
per_task.sort((a, b) => {
|
|
633
|
+
if (a.ES !== b.ES) return a.ES - b.ES;
|
|
634
|
+
return a.id.localeCompare(b.id);
|
|
635
|
+
});
|
|
636
|
+
const critical_path = per_task.filter((task) => task.on_critical_path).map((task) => task.id);
|
|
637
|
+
return {
|
|
638
|
+
critical_path,
|
|
639
|
+
project_duration_hours,
|
|
640
|
+
per_task
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/index/index-manager.ts
|
|
245
645
|
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
246
646
|
var import_node_path = __toESM(require("path"), 1);
|
|
247
647
|
|
|
648
|
+
// src/planning/readiness.ts
|
|
649
|
+
function isResolvedDependencyStatus(status) {
|
|
650
|
+
return status === "done" || status === "canceled";
|
|
651
|
+
}
|
|
652
|
+
function compute_readiness(task, graph) {
|
|
653
|
+
if (task.status === "done" || task.status === "canceled") {
|
|
654
|
+
return "done";
|
|
655
|
+
}
|
|
656
|
+
if (task.status === "in_review") {
|
|
657
|
+
return "waiting_review";
|
|
658
|
+
}
|
|
659
|
+
if (task.status === "in_progress") {
|
|
660
|
+
return "in_progress";
|
|
661
|
+
}
|
|
662
|
+
for (const depId of task.depends_on ?? []) {
|
|
663
|
+
const dep = graph.nodes.get(depId);
|
|
664
|
+
if (!dep || !isResolvedDependencyStatus(dep.status)) {
|
|
665
|
+
return "blocked";
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return "ready";
|
|
669
|
+
}
|
|
670
|
+
function compute_all_readiness(graph) {
|
|
671
|
+
const readiness = /* @__PURE__ */ new Map();
|
|
672
|
+
for (const [taskId, task] of graph.nodes.entries()) {
|
|
673
|
+
readiness.set(taskId, compute_readiness(task, graph));
|
|
674
|
+
}
|
|
675
|
+
return readiness;
|
|
676
|
+
}
|
|
677
|
+
function partition_by_readiness(graph) {
|
|
678
|
+
const partitions = {
|
|
679
|
+
ready: [],
|
|
680
|
+
blocked: [],
|
|
681
|
+
in_progress: [],
|
|
682
|
+
waiting_review: [],
|
|
683
|
+
done: []
|
|
684
|
+
};
|
|
685
|
+
for (const task of graph.nodes.values()) {
|
|
686
|
+
const state = compute_readiness(task, graph);
|
|
687
|
+
partitions[state].push(task);
|
|
688
|
+
}
|
|
689
|
+
return partitions;
|
|
690
|
+
}
|
|
691
|
+
function compute_readiness_with_corrections(graph) {
|
|
692
|
+
const readiness = /* @__PURE__ */ new Map();
|
|
693
|
+
const corrections = [];
|
|
694
|
+
const warnings = [];
|
|
695
|
+
const partitions = {
|
|
696
|
+
ready: [],
|
|
697
|
+
blocked: [],
|
|
698
|
+
in_progress: [],
|
|
699
|
+
waiting_review: [],
|
|
700
|
+
done: []
|
|
701
|
+
};
|
|
702
|
+
for (const [taskId, task] of graph.nodes.entries()) {
|
|
703
|
+
const state = compute_readiness(task, graph);
|
|
704
|
+
readiness.set(taskId, state);
|
|
705
|
+
partitions[state].push(task);
|
|
706
|
+
if (task.status === "blocked" && state === "ready") {
|
|
707
|
+
corrections.push({
|
|
708
|
+
type: "task.transitioned",
|
|
709
|
+
task_id: taskId,
|
|
710
|
+
from: "blocked",
|
|
711
|
+
to: "todo",
|
|
712
|
+
reason: "All dependencies are resolved."
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
if (task.status === "todo" && state === "blocked") {
|
|
716
|
+
const unresolved = (task.depends_on ?? []).filter((depId) => {
|
|
717
|
+
const depTask = graph.nodes.get(depId);
|
|
718
|
+
return !depTask || !isResolvedDependencyStatus(depTask.status);
|
|
719
|
+
});
|
|
720
|
+
warnings.push({
|
|
721
|
+
code: "todo_with_unresolved_dependencies",
|
|
722
|
+
task_id: taskId,
|
|
723
|
+
message: `Task is todo but has unresolved dependencies: ${unresolved.join(", ")}.`
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
readiness,
|
|
729
|
+
partitions,
|
|
730
|
+
corrections,
|
|
731
|
+
warnings
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
248
735
|
// src/parser/delivery-parser.ts
|
|
249
736
|
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
250
737
|
|
|
@@ -412,6 +899,12 @@ function parseYamlFile(filePath) {
|
|
|
412
899
|
const content = import_node_fs2.default.readFileSync(filePath, "utf8");
|
|
413
900
|
return parseYamlContent(content, filePath);
|
|
414
901
|
}
|
|
902
|
+
function stringifyYamlContent(content) {
|
|
903
|
+
return import_yaml2.default.stringify(content);
|
|
904
|
+
}
|
|
905
|
+
function writeYamlFile(filePath, content) {
|
|
906
|
+
import_node_fs2.default.writeFileSync(filePath, stringifyYamlContent(content), "utf8");
|
|
907
|
+
}
|
|
415
908
|
|
|
416
909
|
// src/parser/delivery-parser.ts
|
|
417
910
|
function asStringArray(value) {
|
|
@@ -508,7 +1001,7 @@ function parseTaskFile(filePath) {
|
|
|
508
1001
|
return parseTaskContent(content, filePath);
|
|
509
1002
|
}
|
|
510
1003
|
|
|
511
|
-
// src/
|
|
1004
|
+
// src/index/index-manager.ts
|
|
512
1005
|
function walkFiles(dirPath, extensions) {
|
|
513
1006
|
if (!import_node_fs5.default.existsSync(dirPath)) return [];
|
|
514
1007
|
const out = [];
|
|
@@ -527,106 +1020,447 @@ function walkFiles(dirPath, extensions) {
|
|
|
527
1020
|
}
|
|
528
1021
|
return out.sort((a, b) => a.localeCompare(b));
|
|
529
1022
|
}
|
|
530
|
-
function
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
1023
|
+
function toPosixPath(input) {
|
|
1024
|
+
return input.replace(/\\/g, "/");
|
|
1025
|
+
}
|
|
1026
|
+
function fromRecordMap(value) {
|
|
1027
|
+
return new Map(Object.entries(value));
|
|
1028
|
+
}
|
|
1029
|
+
function toRecordMap(value) {
|
|
1030
|
+
return Object.fromEntries(value.entries());
|
|
1031
|
+
}
|
|
1032
|
+
function round2(value) {
|
|
1033
|
+
return Number(value.toFixed(2));
|
|
1034
|
+
}
|
|
1035
|
+
function parseTaskCollection(files, baseDir) {
|
|
1036
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
1037
|
+
const fileToId = {};
|
|
534
1038
|
for (const filePath of files) {
|
|
535
|
-
const parsed = parseTaskFile(filePath);
|
|
536
|
-
if (
|
|
537
|
-
throw new Error(`Duplicate task id '${parsed.
|
|
1039
|
+
const parsed = parseTaskFile(filePath).task;
|
|
1040
|
+
if (tasks.has(parsed.id)) {
|
|
1041
|
+
throw new Error(`Duplicate task id '${parsed.id}' found at ${filePath}.`);
|
|
538
1042
|
}
|
|
539
|
-
|
|
540
|
-
|
|
1043
|
+
tasks.set(parsed.id, parsed);
|
|
1044
|
+
fileToId[toPosixPath(import_node_path.default.relative(baseDir, filePath))] = parsed.id;
|
|
541
1045
|
}
|
|
542
|
-
return tasks;
|
|
1046
|
+
return { tasks, fileToId };
|
|
543
1047
|
}
|
|
544
|
-
function
|
|
545
|
-
const files = walkFiles(tracksDir, /* @__PURE__ */ new Set([".yml", ".yaml"]));
|
|
1048
|
+
function parseTrackCollection(files, baseDir) {
|
|
546
1049
|
const tracks = /* @__PURE__ */ new Map();
|
|
1050
|
+
const fileToId = {};
|
|
547
1051
|
for (const filePath of files) {
|
|
548
|
-
const
|
|
549
|
-
if (!
|
|
550
|
-
tracks.set(
|
|
1052
|
+
const parsed = parseYamlFile(filePath);
|
|
1053
|
+
if (!parsed.id) continue;
|
|
1054
|
+
tracks.set(parsed.id, parsed);
|
|
1055
|
+
fileToId[toPosixPath(import_node_path.default.relative(baseDir, filePath))] = parsed.id;
|
|
551
1056
|
}
|
|
552
|
-
return tracks;
|
|
1057
|
+
return { tracks, fileToId };
|
|
553
1058
|
}
|
|
554
|
-
function
|
|
555
|
-
const files = walkFiles(resourcesDir, /* @__PURE__ */ new Set([".yml", ".yaml"]));
|
|
1059
|
+
function parseResourceCollection(files, baseDir) {
|
|
556
1060
|
const resources = /* @__PURE__ */ new Map();
|
|
1061
|
+
const fileToId = {};
|
|
557
1062
|
for (const filePath of files) {
|
|
558
|
-
const
|
|
559
|
-
if (!
|
|
560
|
-
resources.set(
|
|
1063
|
+
const parsed = parseYamlFile(filePath);
|
|
1064
|
+
if (!parsed.id) continue;
|
|
1065
|
+
resources.set(parsed.id, parsed);
|
|
1066
|
+
fileToId[toPosixPath(import_node_path.default.relative(baseDir, filePath))] = parsed.id;
|
|
561
1067
|
}
|
|
562
|
-
return resources;
|
|
1068
|
+
return { resources, fileToId };
|
|
563
1069
|
}
|
|
564
|
-
function
|
|
565
|
-
const files = walkFiles(deliveriesDir, /* @__PURE__ */ new Set([".yml", ".yaml", ".md"]));
|
|
1070
|
+
function parseDeliveryCollection(files, baseDir) {
|
|
566
1071
|
const deliveries = /* @__PURE__ */ new Map();
|
|
1072
|
+
const fileToId = {};
|
|
567
1073
|
for (const filePath of files) {
|
|
568
|
-
const parsed = parseDeliveryFile(filePath);
|
|
569
|
-
deliveries.set(parsed.
|
|
1074
|
+
const parsed = parseDeliveryFile(filePath).delivery;
|
|
1075
|
+
deliveries.set(parsed.id, parsed);
|
|
1076
|
+
fileToId[toPosixPath(import_node_path.default.relative(baseDir, filePath))] = parsed.id;
|
|
570
1077
|
}
|
|
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
|
-
});
|
|
1078
|
+
return { deliveries, fileToId };
|
|
583
1079
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1080
|
+
var IndexManager = class {
|
|
1081
|
+
coopDir;
|
|
1082
|
+
indexDir;
|
|
1083
|
+
graphPath;
|
|
1084
|
+
tasksPath;
|
|
1085
|
+
capacityPath;
|
|
1086
|
+
constructor(coopDir) {
|
|
1087
|
+
this.coopDir = import_node_path.default.resolve(coopDir);
|
|
1088
|
+
this.indexDir = import_node_path.default.join(this.coopDir, ".index");
|
|
1089
|
+
this.graphPath = import_node_path.default.join(this.indexDir, "graph.json");
|
|
1090
|
+
this.tasksPath = import_node_path.default.join(this.indexDir, "tasks.json");
|
|
1091
|
+
this.capacityPath = import_node_path.default.join(this.indexDir, "capacity.json");
|
|
1092
|
+
}
|
|
1093
|
+
is_stale() {
|
|
1094
|
+
return this.status().stale;
|
|
1095
|
+
}
|
|
1096
|
+
status() {
|
|
1097
|
+
const scan = this.scanSources();
|
|
1098
|
+
if (!import_node_fs5.default.existsSync(this.graphPath)) {
|
|
1099
|
+
return {
|
|
1100
|
+
exists: false,
|
|
1101
|
+
stale: true,
|
|
1102
|
+
graph_path: this.graphPath,
|
|
1103
|
+
source_max_mtime_ms: scan.sourceMaxMtimeMs,
|
|
1104
|
+
changed_files: Object.keys(scan.sourceFiles).sort((a, b) => a.localeCompare(b)),
|
|
1105
|
+
reason: "graph index missing"
|
|
1106
|
+
};
|
|
605
1107
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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);
|
|
1108
|
+
const indexed = this.readGraphIndexFile();
|
|
1109
|
+
if (!indexed) {
|
|
1110
|
+
return {
|
|
1111
|
+
exists: true,
|
|
1112
|
+
stale: true,
|
|
1113
|
+
graph_path: this.graphPath,
|
|
1114
|
+
source_max_mtime_ms: scan.sourceMaxMtimeMs,
|
|
1115
|
+
changed_files: Object.keys(scan.sourceFiles).sort((a, b) => a.localeCompare(b)),
|
|
1116
|
+
reason: "graph index unreadable"
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
const changed = /* @__PURE__ */ new Set();
|
|
1120
|
+
for (const [relativePath, mtime] of Object.entries(scan.sourceFiles)) {
|
|
1121
|
+
const indexedMtime = indexed.source_files[relativePath];
|
|
1122
|
+
if (indexedMtime === void 0 || indexedMtime !== mtime) {
|
|
1123
|
+
changed.add(relativePath);
|
|
626
1124
|
}
|
|
627
1125
|
}
|
|
628
|
-
|
|
629
|
-
|
|
1126
|
+
for (const relativePath of Object.keys(indexed.source_files)) {
|
|
1127
|
+
if (scan.sourceFiles[relativePath] === void 0) {
|
|
1128
|
+
changed.add(relativePath);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
exists: true,
|
|
1133
|
+
stale: changed.size > 0 || indexed.version !== 1,
|
|
1134
|
+
graph_path: this.graphPath,
|
|
1135
|
+
source_max_mtime_ms: scan.sourceMaxMtimeMs,
|
|
1136
|
+
changed_files: Array.from(changed).sort((a, b) => a.localeCompare(b)),
|
|
1137
|
+
generated_at: indexed.generated_at,
|
|
1138
|
+
reason: indexed.version === 1 ? void 0 : "index version mismatch"
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
load_indexed_graph() {
|
|
1142
|
+
const indexed = this.readGraphIndexFile();
|
|
1143
|
+
if (!indexed) return null;
|
|
1144
|
+
try {
|
|
1145
|
+
return this.deserializeGraph(indexed.graph);
|
|
1146
|
+
} catch {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
build_full_index() {
|
|
1151
|
+
const scan = this.scanSources();
|
|
1152
|
+
const taskData = parseTaskCollection(scan.taskFiles, this.coopDir);
|
|
1153
|
+
const trackData = parseTrackCollection(scan.trackFiles, this.coopDir);
|
|
1154
|
+
const resourceData = parseResourceCollection(scan.resourceFiles, this.coopDir);
|
|
1155
|
+
const deliveryData = parseDeliveryCollection(scan.deliveryFiles, this.coopDir);
|
|
1156
|
+
const graph = build_graph(Array.from(taskData.tasks.values()), {
|
|
1157
|
+
tracks: trackData.tracks,
|
|
1158
|
+
resources: resourceData.resources,
|
|
1159
|
+
deliveries: deliveryData.deliveries
|
|
1160
|
+
});
|
|
1161
|
+
this.writeIndexFiles(
|
|
1162
|
+
graph,
|
|
1163
|
+
scan,
|
|
1164
|
+
{
|
|
1165
|
+
tasks: taskData.fileToId,
|
|
1166
|
+
tracks: trackData.fileToId,
|
|
1167
|
+
resources: resourceData.fileToId,
|
|
1168
|
+
deliveries: deliveryData.fileToId
|
|
1169
|
+
}
|
|
1170
|
+
);
|
|
1171
|
+
return graph;
|
|
1172
|
+
}
|
|
1173
|
+
update_incremental(changedFiles) {
|
|
1174
|
+
const indexed = this.readGraphIndexFile();
|
|
1175
|
+
if (!indexed) {
|
|
1176
|
+
return this.build_full_index();
|
|
1177
|
+
}
|
|
1178
|
+
const scan = this.scanSources();
|
|
1179
|
+
const changed = changedFiles.length > 0 ? changedFiles : this.status().changed_files;
|
|
1180
|
+
const normalized = new Set(
|
|
1181
|
+
changed.map((entry) => this.normalizeChangedFile(entry)).filter((entry) => Boolean(entry))
|
|
1182
|
+
);
|
|
1183
|
+
if (normalized.size === 0) {
|
|
1184
|
+
const fromIndex = this.load_indexed_graph();
|
|
1185
|
+
return fromIndex ?? this.build_full_index();
|
|
1186
|
+
}
|
|
1187
|
+
const taskMap = fromRecordMap(indexed.graph.nodes);
|
|
1188
|
+
const trackMap = fromRecordMap(indexed.graph.tracks);
|
|
1189
|
+
const resourceMap = fromRecordMap(indexed.graph.resources);
|
|
1190
|
+
const deliveryMap = fromRecordMap(indexed.graph.deliveries);
|
|
1191
|
+
const fileMaps = {
|
|
1192
|
+
tasks: { ...indexed.files.tasks },
|
|
1193
|
+
tracks: { ...indexed.files.tracks },
|
|
1194
|
+
resources: { ...indexed.files.resources },
|
|
1195
|
+
deliveries: { ...indexed.files.deliveries }
|
|
1196
|
+
};
|
|
1197
|
+
for (const relativePath of normalized) {
|
|
1198
|
+
const kind = this.fileKind(relativePath);
|
|
1199
|
+
if (!kind) {
|
|
1200
|
+
return this.build_full_index();
|
|
1201
|
+
}
|
|
1202
|
+
const absolutePath = import_node_path.default.join(this.coopDir, relativePath);
|
|
1203
|
+
const exists = import_node_fs5.default.existsSync(absolutePath);
|
|
1204
|
+
if (!exists) {
|
|
1205
|
+
const oldId = fileMaps[kind][relativePath];
|
|
1206
|
+
if (!oldId) {
|
|
1207
|
+
return this.build_full_index();
|
|
1208
|
+
}
|
|
1209
|
+
delete fileMaps[kind][relativePath];
|
|
1210
|
+
if (kind === "tasks") taskMap.delete(oldId);
|
|
1211
|
+
if (kind === "tracks") trackMap.delete(oldId);
|
|
1212
|
+
if (kind === "resources") resourceMap.delete(oldId);
|
|
1213
|
+
if (kind === "deliveries") deliveryMap.delete(oldId);
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
if (kind === "tasks") {
|
|
1218
|
+
const oldId2 = fileMaps.tasks[relativePath];
|
|
1219
|
+
if (oldId2) taskMap.delete(oldId2);
|
|
1220
|
+
const parsed2 = parseTaskFile(absolutePath).task;
|
|
1221
|
+
taskMap.set(parsed2.id, parsed2);
|
|
1222
|
+
fileMaps.tasks[relativePath] = parsed2.id;
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (kind === "tracks") {
|
|
1226
|
+
const oldId2 = fileMaps.tracks[relativePath];
|
|
1227
|
+
if (oldId2) trackMap.delete(oldId2);
|
|
1228
|
+
const parsed2 = parseYamlFile(absolutePath);
|
|
1229
|
+
if (parsed2.id) {
|
|
1230
|
+
trackMap.set(parsed2.id, parsed2);
|
|
1231
|
+
fileMaps.tracks[relativePath] = parsed2.id;
|
|
1232
|
+
}
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (kind === "resources") {
|
|
1236
|
+
const oldId2 = fileMaps.resources[relativePath];
|
|
1237
|
+
if (oldId2) resourceMap.delete(oldId2);
|
|
1238
|
+
const parsed2 = parseYamlFile(absolutePath);
|
|
1239
|
+
if (parsed2.id) {
|
|
1240
|
+
resourceMap.set(parsed2.id, parsed2);
|
|
1241
|
+
fileMaps.resources[relativePath] = parsed2.id;
|
|
1242
|
+
}
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
const oldId = fileMaps.deliveries[relativePath];
|
|
1246
|
+
if (oldId) deliveryMap.delete(oldId);
|
|
1247
|
+
const parsed = parseDeliveryFile(absolutePath).delivery;
|
|
1248
|
+
deliveryMap.set(parsed.id, parsed);
|
|
1249
|
+
fileMaps.deliveries[relativePath] = parsed.id;
|
|
1250
|
+
} catch {
|
|
1251
|
+
return this.build_full_index();
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const graph = build_graph(Array.from(taskMap.values()), {
|
|
1255
|
+
tracks: trackMap,
|
|
1256
|
+
resources: resourceMap,
|
|
1257
|
+
deliveries: deliveryMap
|
|
1258
|
+
});
|
|
1259
|
+
this.writeIndexFiles(graph, scan, fileMaps);
|
|
1260
|
+
return graph;
|
|
1261
|
+
}
|
|
1262
|
+
ensureIndexDir() {
|
|
1263
|
+
import_node_fs5.default.mkdirSync(this.indexDir, { recursive: true });
|
|
1264
|
+
}
|
|
1265
|
+
writeIndexFiles(graph, scan, fileMaps) {
|
|
1266
|
+
this.ensureIndexDir();
|
|
1267
|
+
const graphIndex = {
|
|
1268
|
+
version: 1,
|
|
1269
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1270
|
+
source_max_mtime_ms: scan.sourceMaxMtimeMs,
|
|
1271
|
+
source_files: scan.sourceFiles,
|
|
1272
|
+
files: fileMaps,
|
|
1273
|
+
graph: this.serializeGraph(graph)
|
|
1274
|
+
};
|
|
1275
|
+
import_node_fs5.default.writeFileSync(this.graphPath, `${JSON.stringify(graphIndex, null, 2)}
|
|
1276
|
+
`, "utf8");
|
|
1277
|
+
import_node_fs5.default.writeFileSync(this.tasksPath, `${JSON.stringify(this.computeTaskIndexRows(graph), null, 2)}
|
|
1278
|
+
`, "utf8");
|
|
1279
|
+
import_node_fs5.default.writeFileSync(
|
|
1280
|
+
this.capacityPath,
|
|
1281
|
+
`${JSON.stringify(this.computeCapacitySnapshot(graph.resources), null, 2)}
|
|
1282
|
+
`,
|
|
1283
|
+
"utf8"
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
readGraphIndexFile() {
|
|
1287
|
+
if (!import_node_fs5.default.existsSync(this.graphPath)) return null;
|
|
1288
|
+
try {
|
|
1289
|
+
const parsed = JSON.parse(import_node_fs5.default.readFileSync(this.graphPath, "utf8"));
|
|
1290
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1291
|
+
if (!parsed.graph || typeof parsed.graph !== "object") return null;
|
|
1292
|
+
if (!parsed.source_files || typeof parsed.source_files !== "object") return null;
|
|
1293
|
+
if (!parsed.files || typeof parsed.files !== "object") return null;
|
|
1294
|
+
return parsed;
|
|
1295
|
+
} catch {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
serializeGraph(graph) {
|
|
1300
|
+
return {
|
|
1301
|
+
nodes: toRecordMap(graph.nodes),
|
|
1302
|
+
forward: Object.fromEntries(
|
|
1303
|
+
Array.from(graph.forward.entries()).map(([id, deps]) => [id, Array.from(deps).sort((a, b) => a.localeCompare(b))])
|
|
1304
|
+
),
|
|
1305
|
+
reverse: Object.fromEntries(
|
|
1306
|
+
Array.from(graph.reverse.entries()).map(([id, deps]) => [id, Array.from(deps).sort((a, b) => a.localeCompare(b))])
|
|
1307
|
+
),
|
|
1308
|
+
topological_order: [...graph.topological_order],
|
|
1309
|
+
tracks: toRecordMap(graph.tracks),
|
|
1310
|
+
resources: toRecordMap(graph.resources),
|
|
1311
|
+
deliveries: toRecordMap(graph.deliveries),
|
|
1312
|
+
external_dependencies: Object.fromEntries(
|
|
1313
|
+
Array.from(graph.external_dependencies?.entries() ?? []).map(([id, deps]) => [id, deps])
|
|
1314
|
+
)
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
deserializeGraph(data) {
|
|
1318
|
+
return {
|
|
1319
|
+
nodes: fromRecordMap(data.nodes ?? {}),
|
|
1320
|
+
forward: new Map(
|
|
1321
|
+
Object.entries(data.forward ?? {}).map(([id, deps]) => [id, new Set(deps ?? [])])
|
|
1322
|
+
),
|
|
1323
|
+
reverse: new Map(
|
|
1324
|
+
Object.entries(data.reverse ?? {}).map(([id, deps]) => [id, new Set(deps ?? [])])
|
|
1325
|
+
),
|
|
1326
|
+
topological_order: Array.isArray(data.topological_order) ? [...data.topological_order] : [],
|
|
1327
|
+
tracks: fromRecordMap(data.tracks ?? {}),
|
|
1328
|
+
resources: fromRecordMap(data.resources ?? {}),
|
|
1329
|
+
deliveries: fromRecordMap(data.deliveries ?? {}),
|
|
1330
|
+
external_dependencies: new Map(Object.entries(data.external_dependencies ?? {}))
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
scanSources() {
|
|
1334
|
+
const taskFiles = walkFiles(import_node_path.default.join(this.coopDir, "tasks"), /* @__PURE__ */ new Set([".md"]));
|
|
1335
|
+
const trackFiles = walkFiles(import_node_path.default.join(this.coopDir, "tracks"), /* @__PURE__ */ new Set([".yml", ".yaml"]));
|
|
1336
|
+
const resourceFiles = walkFiles(import_node_path.default.join(this.coopDir, "resources"), /* @__PURE__ */ new Set([".yml", ".yaml"]));
|
|
1337
|
+
const deliveryFiles = walkFiles(import_node_path.default.join(this.coopDir, "deliveries"), /* @__PURE__ */ new Set([".yml", ".yaml", ".md"]));
|
|
1338
|
+
const sourceFiles = {};
|
|
1339
|
+
let sourceMaxMtimeMs = 0;
|
|
1340
|
+
const allFiles = [...taskFiles, ...trackFiles, ...resourceFiles, ...deliveryFiles];
|
|
1341
|
+
for (const filePath of allFiles) {
|
|
1342
|
+
const stat = import_node_fs5.default.statSync(filePath);
|
|
1343
|
+
const relative = toPosixPath(import_node_path.default.relative(this.coopDir, filePath));
|
|
1344
|
+
sourceFiles[relative] = stat.mtimeMs;
|
|
1345
|
+
if (stat.mtimeMs > sourceMaxMtimeMs) {
|
|
1346
|
+
sourceMaxMtimeMs = stat.mtimeMs;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return {
|
|
1350
|
+
taskFiles,
|
|
1351
|
+
trackFiles,
|
|
1352
|
+
resourceFiles,
|
|
1353
|
+
deliveryFiles,
|
|
1354
|
+
sourceFiles,
|
|
1355
|
+
sourceMaxMtimeMs
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
normalizeChangedFile(value) {
|
|
1359
|
+
if (!value || !value.trim()) return null;
|
|
1360
|
+
const normalized = import_node_path.default.normalize(value.trim());
|
|
1361
|
+
const absolute = import_node_path.default.isAbsolute(normalized) ? normalized : import_node_path.default.resolve(this.coopDir, normalized);
|
|
1362
|
+
const relative = import_node_path.default.relative(this.coopDir, absolute);
|
|
1363
|
+
if (relative.startsWith("..")) return null;
|
|
1364
|
+
return toPosixPath(relative);
|
|
1365
|
+
}
|
|
1366
|
+
fileKind(relativePath) {
|
|
1367
|
+
if (relativePath.startsWith("tasks/")) return "tasks";
|
|
1368
|
+
if (relativePath.startsWith("tracks/")) return "tracks";
|
|
1369
|
+
if (relativePath.startsWith("resources/")) return "resources";
|
|
1370
|
+
if (relativePath.startsWith("deliveries/")) return "deliveries";
|
|
1371
|
+
return null;
|
|
1372
|
+
}
|
|
1373
|
+
computeTaskIndexRows(graph) {
|
|
1374
|
+
const depth = /* @__PURE__ */ new Map();
|
|
1375
|
+
for (const id of graph.topological_order) {
|
|
1376
|
+
const deps = Array.from(graph.forward.get(id) ?? /* @__PURE__ */ new Set()).filter((dep) => graph.nodes.has(dep));
|
|
1377
|
+
if (deps.length === 0) {
|
|
1378
|
+
depth.set(id, 0);
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
const maxDepDepth = Math.max(...deps.map((dep) => depth.get(dep) ?? 0));
|
|
1382
|
+
depth.set(id, maxDepDepth + 1);
|
|
1383
|
+
}
|
|
1384
|
+
const rows = {};
|
|
1385
|
+
for (const [id, task] of graph.nodes.entries()) {
|
|
1386
|
+
const estimate = task.estimate;
|
|
1387
|
+
let pertHours;
|
|
1388
|
+
let pertStddev;
|
|
1389
|
+
if (estimate && typeof estimate.optimistic_hours === "number" && typeof estimate.expected_hours === "number" && typeof estimate.pessimistic_hours === "number") {
|
|
1390
|
+
pertHours = round2((estimate.optimistic_hours + 4 * estimate.expected_hours + estimate.pessimistic_hours) / 6);
|
|
1391
|
+
pertStddev = round2((estimate.pessimistic_hours - estimate.optimistic_hours) / 6);
|
|
1392
|
+
}
|
|
1393
|
+
const blocks = Array.from(graph.reverse.get(id) ?? /* @__PURE__ */ new Set()).sort((a, b) => a.localeCompare(b));
|
|
1394
|
+
const row = {
|
|
1395
|
+
id,
|
|
1396
|
+
status: task.status,
|
|
1397
|
+
priority: task.priority ?? null,
|
|
1398
|
+
track: task.track ?? null,
|
|
1399
|
+
blocks,
|
|
1400
|
+
readiness: compute_readiness(task, graph),
|
|
1401
|
+
depth: depth.get(id) ?? 0
|
|
1402
|
+
};
|
|
1403
|
+
if (pertHours !== void 0) row.pert_hours = pertHours;
|
|
1404
|
+
if (pertStddev !== void 0) row.pert_stddev = pertStddev;
|
|
1405
|
+
rows[id] = row;
|
|
1406
|
+
}
|
|
1407
|
+
return rows;
|
|
1408
|
+
}
|
|
1409
|
+
computeCapacitySnapshot(resources) {
|
|
1410
|
+
const profiles = {};
|
|
1411
|
+
for (const [id, profile] of resources.entries()) {
|
|
1412
|
+
if (profile.type === "human") {
|
|
1413
|
+
const members = profile.members ?? [];
|
|
1414
|
+
const baseHours = members.reduce((sum, member) => sum + (member.hours_per_week ?? 0), 0);
|
|
1415
|
+
const meetings = profile.overhead?.meetings_percent ?? 0;
|
|
1416
|
+
const context = profile.overhead?.context_switch_percent ?? 0;
|
|
1417
|
+
const overheadPercent = meetings + context;
|
|
1418
|
+
const effectiveHours = baseHours * Math.max(0, 1 - overheadPercent / 100);
|
|
1419
|
+
profiles[id] = {
|
|
1420
|
+
id,
|
|
1421
|
+
type: profile.type,
|
|
1422
|
+
members: members.length,
|
|
1423
|
+
total_weekly_hours: round2(baseHours),
|
|
1424
|
+
effective_weekly_hours: round2(effectiveHours),
|
|
1425
|
+
overhead_percent: round2(overheadPercent)
|
|
1426
|
+
};
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
if (profile.type === "ai") {
|
|
1430
|
+
profiles[id] = {
|
|
1431
|
+
id,
|
|
1432
|
+
type: profile.type,
|
|
1433
|
+
agents: profile.agents.length,
|
|
1434
|
+
effective_capacity: profile.effective_capacity ?? null
|
|
1435
|
+
};
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
profiles[id] = {
|
|
1439
|
+
id,
|
|
1440
|
+
type: profile.type,
|
|
1441
|
+
nodes: profile.nodes.length,
|
|
1442
|
+
effective_capacity: profile.effective_capacity ?? null
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
return {
|
|
1446
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1447
|
+
profiles
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
// src/graph/loader.ts
|
|
1453
|
+
function load_graph(coopDir) {
|
|
1454
|
+
const index = new IndexManager(coopDir);
|
|
1455
|
+
const status = index.status();
|
|
1456
|
+
if (status.exists && !status.stale) {
|
|
1457
|
+
const cached = index.load_indexed_graph();
|
|
1458
|
+
if (cached) return cached;
|
|
1459
|
+
}
|
|
1460
|
+
if (!status.exists) {
|
|
1461
|
+
return index.build_full_index();
|
|
1462
|
+
}
|
|
1463
|
+
return index.update_incremental(status.changed_files);
|
|
630
1464
|
}
|
|
631
1465
|
|
|
632
1466
|
// src/graph/validator.ts
|
|
@@ -692,10 +1526,17 @@ function checkTerminalConvergence(graph) {
|
|
|
692
1526
|
}
|
|
693
1527
|
return issues;
|
|
694
1528
|
}
|
|
695
|
-
function validate_graph(graph) {
|
|
1529
|
+
function validate_graph(graph, context = {}) {
|
|
696
1530
|
const issues = [];
|
|
697
1531
|
const cycle = detect_cycle(graph);
|
|
698
1532
|
if (cycle) {
|
|
1533
|
+
context.eventEmitter?.emit({
|
|
1534
|
+
type: "graph.cycle_detected",
|
|
1535
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1536
|
+
payload: {
|
|
1537
|
+
cycle
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
699
1540
|
issues.push(error("acyclicity", `Dependency cycle detected: ${cycle.join(" -> ")}.`, cycle));
|
|
700
1541
|
}
|
|
701
1542
|
for (const [taskId, deps] of graph.forward.entries()) {
|
|
@@ -733,8 +1574,540 @@ function validate_graph(graph) {
|
|
|
733
1574
|
return issues;
|
|
734
1575
|
}
|
|
735
1576
|
|
|
736
|
-
// src/
|
|
1577
|
+
// src/events/emitter.ts
|
|
1578
|
+
var CoopEventEmitter = class {
|
|
1579
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1580
|
+
on(eventType, handler) {
|
|
1581
|
+
const existing = this.handlers.get(eventType) ?? /* @__PURE__ */ new Set();
|
|
1582
|
+
existing.add(handler);
|
|
1583
|
+
this.handlers.set(eventType, existing);
|
|
1584
|
+
return () => {
|
|
1585
|
+
const bucket = this.handlers.get(eventType);
|
|
1586
|
+
if (!bucket) return;
|
|
1587
|
+
bucket.delete(handler);
|
|
1588
|
+
if (bucket.size === 0) {
|
|
1589
|
+
this.handlers.delete(eventType);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
emit(event) {
|
|
1594
|
+
const listeners = this.handlers.get(event.type);
|
|
1595
|
+
if (!listeners || listeners.size === 0) return;
|
|
1596
|
+
for (const listener of listeners) {
|
|
1597
|
+
listener(event);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
// src/events/events.ts
|
|
1603
|
+
var COOP_EVENT_TYPES = [
|
|
1604
|
+
"task.created",
|
|
1605
|
+
"task.transitioned",
|
|
1606
|
+
"task.assigned",
|
|
1607
|
+
"delivery.committed",
|
|
1608
|
+
"delivery.at_risk",
|
|
1609
|
+
"run.started",
|
|
1610
|
+
"run.completed",
|
|
1611
|
+
"run.failed",
|
|
1612
|
+
"graph.cycle_detected"
|
|
1613
|
+
];
|
|
1614
|
+
|
|
1615
|
+
// src/events/hook-runner.ts
|
|
1616
|
+
var import_node_child_process = require("child_process");
|
|
1617
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
1618
|
+
function commandFor(hookPath) {
|
|
1619
|
+
const ext = import_node_path2.default.extname(hookPath).toLowerCase();
|
|
1620
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
1621
|
+
return {
|
|
1622
|
+
command: process.execPath,
|
|
1623
|
+
args: [hookPath]
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
return {
|
|
1627
|
+
command: hookPath,
|
|
1628
|
+
args: []
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
function run_hook(hookPath, event, timeoutMs = 1e4) {
|
|
1632
|
+
return new Promise((resolve) => {
|
|
1633
|
+
const payload = JSON.stringify(event);
|
|
1634
|
+
const runner = commandFor(hookPath);
|
|
1635
|
+
const child = (0, import_node_child_process.spawn)(runner.command, runner.args, {
|
|
1636
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1637
|
+
shell: runner.command === hookPath,
|
|
1638
|
+
windowsHide: true
|
|
1639
|
+
});
|
|
1640
|
+
let stdout = "";
|
|
1641
|
+
let stderr = "";
|
|
1642
|
+
let settled = false;
|
|
1643
|
+
let timedOut = false;
|
|
1644
|
+
const settle = (result) => {
|
|
1645
|
+
if (settled) return;
|
|
1646
|
+
settled = true;
|
|
1647
|
+
resolve(result);
|
|
1648
|
+
};
|
|
1649
|
+
const timeout = setTimeout(() => {
|
|
1650
|
+
timedOut = true;
|
|
1651
|
+
child.kill("SIGTERM");
|
|
1652
|
+
settle({
|
|
1653
|
+
exitCode: null,
|
|
1654
|
+
stdout,
|
|
1655
|
+
stderr: `${stderr}${stderr ? "\n" : ""}Hook timed out after ${timeoutMs}ms.`,
|
|
1656
|
+
timedOut: true
|
|
1657
|
+
});
|
|
1658
|
+
}, timeoutMs);
|
|
1659
|
+
child.stdout.on("data", (chunk) => {
|
|
1660
|
+
stdout += chunk.toString();
|
|
1661
|
+
});
|
|
1662
|
+
child.stderr.on("data", (chunk) => {
|
|
1663
|
+
stderr += chunk.toString();
|
|
1664
|
+
});
|
|
1665
|
+
child.on("error", (error6) => {
|
|
1666
|
+
clearTimeout(timeout);
|
|
1667
|
+
settle({
|
|
1668
|
+
exitCode: -1,
|
|
1669
|
+
stdout,
|
|
1670
|
+
stderr: `${stderr}${stderr ? "\n" : ""}${error6.message}`,
|
|
1671
|
+
timedOut: false
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1674
|
+
child.on("close", (code) => {
|
|
1675
|
+
clearTimeout(timeout);
|
|
1676
|
+
settle({
|
|
1677
|
+
exitCode: timedOut ? null : code,
|
|
1678
|
+
stdout,
|
|
1679
|
+
stderr,
|
|
1680
|
+
timedOut
|
|
1681
|
+
});
|
|
1682
|
+
});
|
|
1683
|
+
child.stdin.write(payload, "utf8");
|
|
1684
|
+
child.stdin.end();
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/auth/auth.ts
|
|
1689
|
+
var DEFAULT_AUTH_CONFIG = {
|
|
1690
|
+
enabled: false,
|
|
1691
|
+
roles: {
|
|
1692
|
+
admin: { permissions: ["all"] },
|
|
1693
|
+
lead: {
|
|
1694
|
+
permissions: ["create_task", "transition_task", "create_delivery", "plan_delivery", "assign_task"]
|
|
1695
|
+
},
|
|
1696
|
+
contributor: { permissions: ["transition_own_task", "comment"] },
|
|
1697
|
+
viewer: { permissions: ["read"] }
|
|
1698
|
+
},
|
|
1699
|
+
assignments: {},
|
|
1700
|
+
policies: [
|
|
1701
|
+
{ action: "delete_task", requires: ["admin"] },
|
|
1702
|
+
{ action: "commit_delivery", requires: ["admin", "lead"] },
|
|
1703
|
+
{ action: "override_blocked", requires: ["admin", "lead"] }
|
|
1704
|
+
]
|
|
1705
|
+
};
|
|
1706
|
+
function normalizeRole(value) {
|
|
1707
|
+
if (typeof value !== "string") return null;
|
|
1708
|
+
if (value === "admin" || value === "lead" || value === "contributor" || value === "viewer") return value;
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
function asPermissions(value) {
|
|
1712
|
+
if (!Array.isArray(value)) return [];
|
|
1713
|
+
return value.map((entry) => String(entry)).filter((entry) => {
|
|
1714
|
+
return entry === "all" || entry === "read" || entry === "comment" || entry === "create_task" || entry === "transition_task" || entry === "transition_own_task" || entry === "create_delivery" || entry === "plan_delivery" || entry === "assign_task" || entry === "delete_task" || entry === "commit_delivery" || entry === "override_blocked";
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
function normalizeUser(value) {
|
|
1718
|
+
return value.trim().toLowerCase();
|
|
1719
|
+
}
|
|
1720
|
+
function loadPolicies(raw) {
|
|
1721
|
+
if (!Array.isArray(raw)) return [...DEFAULT_AUTH_CONFIG.policies];
|
|
1722
|
+
const out = [];
|
|
1723
|
+
for (const entry of raw) {
|
|
1724
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1725
|
+
const record = entry;
|
|
1726
|
+
const action = typeof record.action === "string" ? record.action : "";
|
|
1727
|
+
if (action !== "delete_task" && action !== "commit_delivery" && action !== "override_blocked" && action !== "transition_task" && action !== "create_delivery") {
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
const requiresRaw = record.requires;
|
|
1731
|
+
const requiresList = Array.isArray(requiresRaw) ? requiresRaw : [requiresRaw];
|
|
1732
|
+
const requires = requiresList.map((role) => normalizeRole(role)).filter((role) => Boolean(role));
|
|
1733
|
+
if (requires.length === 0) continue;
|
|
1734
|
+
out.push({ action, requires });
|
|
1735
|
+
}
|
|
1736
|
+
return out.length > 0 ? out : [...DEFAULT_AUTH_CONFIG.policies];
|
|
1737
|
+
}
|
|
1738
|
+
function load_auth_config(config) {
|
|
1739
|
+
const source = config && typeof config === "object" ? config : {};
|
|
1740
|
+
const rawAuth = source.authorization && typeof source.authorization === "object" ? source.authorization : {};
|
|
1741
|
+
const enabled = Boolean(rawAuth.enabled ?? false);
|
|
1742
|
+
const rawRoles = rawAuth.roles && typeof rawAuth.roles === "object" ? rawAuth.roles : {};
|
|
1743
|
+
const roles = {
|
|
1744
|
+
admin: { permissions: [...DEFAULT_AUTH_CONFIG.roles.admin.permissions] },
|
|
1745
|
+
lead: { permissions: [...DEFAULT_AUTH_CONFIG.roles.lead.permissions] },
|
|
1746
|
+
contributor: { permissions: [...DEFAULT_AUTH_CONFIG.roles.contributor.permissions] },
|
|
1747
|
+
viewer: { permissions: [...DEFAULT_AUTH_CONFIG.roles.viewer.permissions] }
|
|
1748
|
+
};
|
|
1749
|
+
for (const role of Object.keys(roles)) {
|
|
1750
|
+
const roleConfig = rawRoles[role];
|
|
1751
|
+
if (!roleConfig || typeof roleConfig !== "object") continue;
|
|
1752
|
+
const permissions = asPermissions(roleConfig.permissions);
|
|
1753
|
+
if (permissions.length > 0) {
|
|
1754
|
+
roles[role].permissions = permissions;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
const rawAssignments = rawAuth.assignments && typeof rawAuth.assignments === "object" ? rawAuth.assignments : {};
|
|
1758
|
+
const assignments = {};
|
|
1759
|
+
for (const [user, roleRaw] of Object.entries(rawAssignments)) {
|
|
1760
|
+
const role = normalizeRole(roleRaw);
|
|
1761
|
+
if (!role) continue;
|
|
1762
|
+
assignments[normalizeUser(user)] = role;
|
|
1763
|
+
}
|
|
1764
|
+
const policies = loadPolicies(rawAuth.policies);
|
|
1765
|
+
return {
|
|
1766
|
+
enabled,
|
|
1767
|
+
roles,
|
|
1768
|
+
assignments,
|
|
1769
|
+
policies
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
function get_user_role(user, config) {
|
|
1773
|
+
const key = normalizeUser(user);
|
|
1774
|
+
return config.assignments[key] ?? "viewer";
|
|
1775
|
+
}
|
|
1776
|
+
function hasRolePolicyOverride(role, action, config) {
|
|
1777
|
+
const policy = config.policies.find((entry) => entry.action === action);
|
|
1778
|
+
if (!policy) return true;
|
|
1779
|
+
return policy.requires.includes(role);
|
|
1780
|
+
}
|
|
1781
|
+
function check_permission(user, action, context) {
|
|
1782
|
+
const { config } = context;
|
|
1783
|
+
if (!config.enabled) return true;
|
|
1784
|
+
const role = get_user_role(user, config);
|
|
1785
|
+
const rolePermissions = new Set(config.roles[role]?.permissions ?? []);
|
|
1786
|
+
let allowed = rolePermissions.has("all") || rolePermissions.has(action);
|
|
1787
|
+
if (!allowed && action === "transition_task" && rolePermissions.has("transition_own_task")) {
|
|
1788
|
+
const owner = context.taskOwner?.trim() ?? "";
|
|
1789
|
+
allowed = owner.length > 0 && normalizeUser(owner) === normalizeUser(user);
|
|
1790
|
+
}
|
|
1791
|
+
if (!allowed) return false;
|
|
1792
|
+
return hasRolePolicyOverride(role, action, config);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// src/plugins/plugin-loader.ts
|
|
737
1796
|
var import_node_fs6 = __toESM(require("fs"), 1);
|
|
1797
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
1798
|
+
var EVENT_TYPES = new Set(COOP_EVENT_TYPES);
|
|
1799
|
+
var PLUGIN_ID_PATTERN = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/i;
|
|
1800
|
+
function pluginDir(coopDir) {
|
|
1801
|
+
return import_node_path3.default.join(coopDir, "plugins");
|
|
1802
|
+
}
|
|
1803
|
+
function listPluginFiles(coopDir) {
|
|
1804
|
+
const dir = pluginDir(coopDir);
|
|
1805
|
+
if (!import_node_fs6.default.existsSync(dir)) return [];
|
|
1806
|
+
const entries = import_node_fs6.default.readdirSync(dir, { withFileTypes: true });
|
|
1807
|
+
return entries.filter((entry) => entry.isFile() && /\.(yml|yaml)$/i.test(entry.name)).map((entry) => import_node_path3.default.join(dir, entry.name)).sort((a, b) => a.localeCompare(b));
|
|
1808
|
+
}
|
|
1809
|
+
function asString(value, field, source) {
|
|
1810
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1811
|
+
throw new Error(`${source}: ${field} must be a non-empty string.`);
|
|
1812
|
+
}
|
|
1813
|
+
return value.trim();
|
|
1814
|
+
}
|
|
1815
|
+
function asStringMap(value, field, source) {
|
|
1816
|
+
if (value === void 0) return {};
|
|
1817
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1818
|
+
throw new Error(`${source}: ${field} must be a mapping of string values.`);
|
|
1819
|
+
}
|
|
1820
|
+
const out = {};
|
|
1821
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1822
|
+
if (typeof entry !== "string") {
|
|
1823
|
+
throw new Error(`${source}: ${field}.${key} must be a string.`);
|
|
1824
|
+
}
|
|
1825
|
+
out[key] = entry;
|
|
1826
|
+
}
|
|
1827
|
+
return out;
|
|
1828
|
+
}
|
|
1829
|
+
function parseAction(raw, source) {
|
|
1830
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1831
|
+
throw new Error(`${source}: trigger action must be a mapping object.`);
|
|
1832
|
+
}
|
|
1833
|
+
const record = raw;
|
|
1834
|
+
const actionType = asString(record.type, "action.type", source).toLowerCase();
|
|
1835
|
+
if (actionType === "webhook") {
|
|
1836
|
+
const url = asString(record.url, "action.url", source);
|
|
1837
|
+
const template = record.template === void 0 ? void 0 : asString(record.template, "action.template", source);
|
|
1838
|
+
const headers = asStringMap(record.headers, "action.headers", source);
|
|
1839
|
+
return { type: "webhook", url, template, headers };
|
|
1840
|
+
}
|
|
1841
|
+
if (actionType === "console") {
|
|
1842
|
+
const template = asString(record.template, "action.template", source);
|
|
1843
|
+
return { type: "console", template };
|
|
1844
|
+
}
|
|
1845
|
+
if (actionType === "github_pr") {
|
|
1846
|
+
const operation = asString(record.operation, "action.operation", source).toLowerCase();
|
|
1847
|
+
if (operation !== "create_or_update" && operation !== "merge") {
|
|
1848
|
+
throw new Error(`${source}: unsupported action.operation '${operation}' for github_pr.`);
|
|
1849
|
+
}
|
|
1850
|
+
const branch = record.branch === void 0 ? void 0 : asString(record.branch, "action.branch", source);
|
|
1851
|
+
const baseBranch = record.base_branch === void 0 ? void 0 : asString(record.base_branch, "action.base_branch", source);
|
|
1852
|
+
const draft = record.draft === void 0 ? void 0 : Boolean(record.draft);
|
|
1853
|
+
return {
|
|
1854
|
+
type: "github_pr",
|
|
1855
|
+
operation,
|
|
1856
|
+
branch,
|
|
1857
|
+
base_branch: baseBranch,
|
|
1858
|
+
draft
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
throw new Error(`${source}: unsupported action.type '${actionType}'.`);
|
|
1862
|
+
}
|
|
1863
|
+
function parseTrigger(raw, source, index) {
|
|
1864
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1865
|
+
throw new Error(`${source}: triggers[${index}] must be a mapping object.`);
|
|
1866
|
+
}
|
|
1867
|
+
const record = raw;
|
|
1868
|
+
const event = asString(record.event, `triggers[${index}].event`, source);
|
|
1869
|
+
if (!EVENT_TYPES.has(event)) {
|
|
1870
|
+
throw new Error(`${source}: unsupported event '${event}' in triggers[${index}].`);
|
|
1871
|
+
}
|
|
1872
|
+
const filterRaw = record.filter;
|
|
1873
|
+
if (filterRaw !== void 0 && (!filterRaw || typeof filterRaw !== "object" || Array.isArray(filterRaw))) {
|
|
1874
|
+
throw new Error(`${source}: triggers[${index}].filter must be a mapping object.`);
|
|
1875
|
+
}
|
|
1876
|
+
const action = parseAction(record.action, `${source} triggers[${index}]`);
|
|
1877
|
+
return {
|
|
1878
|
+
event,
|
|
1879
|
+
filter: filterRaw,
|
|
1880
|
+
action
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function parseSecrets(value, source) {
|
|
1884
|
+
if (value === void 0) return [];
|
|
1885
|
+
if (!Array.isArray(value)) {
|
|
1886
|
+
throw new Error(`${source}: secrets must be a list of environment variable names.`);
|
|
1887
|
+
}
|
|
1888
|
+
return value.map((entry, index) => asString(entry, `secrets[${index}]`, source));
|
|
1889
|
+
}
|
|
1890
|
+
function parsePlugin(filePath) {
|
|
1891
|
+
const source = import_node_path3.default.relative(import_node_path3.default.dirname(import_node_path3.default.dirname(filePath)), filePath);
|
|
1892
|
+
const raw = parseYamlFile(filePath);
|
|
1893
|
+
const id = asString(raw.id, "id", source);
|
|
1894
|
+
if (!PLUGIN_ID_PATTERN.test(id)) {
|
|
1895
|
+
throw new Error(`${source}: id '${id}' contains invalid characters.`);
|
|
1896
|
+
}
|
|
1897
|
+
const name = asString(raw.name, "name", source);
|
|
1898
|
+
const version = asString(raw.version, "version", source);
|
|
1899
|
+
if (!Array.isArray(raw.triggers) || raw.triggers.length === 0) {
|
|
1900
|
+
throw new Error(`${source}: triggers must be a non-empty list.`);
|
|
1901
|
+
}
|
|
1902
|
+
const triggers = raw.triggers.map((entry, index) => parseTrigger(entry, source, index));
|
|
1903
|
+
const secrets = parseSecrets(raw.secrets, source);
|
|
1904
|
+
return {
|
|
1905
|
+
id,
|
|
1906
|
+
name,
|
|
1907
|
+
version,
|
|
1908
|
+
triggers,
|
|
1909
|
+
secrets,
|
|
1910
|
+
file_path: filePath
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
function load_plugins(coopDir) {
|
|
1914
|
+
const files = listPluginFiles(coopDir);
|
|
1915
|
+
const plugins = files.map((filePath) => parsePlugin(filePath));
|
|
1916
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1917
|
+
for (const plugin of plugins) {
|
|
1918
|
+
if (seen.has(plugin.id)) {
|
|
1919
|
+
throw new Error(`Duplicate plugin id '${plugin.id}'.`);
|
|
1920
|
+
}
|
|
1921
|
+
seen.add(plugin.id);
|
|
1922
|
+
}
|
|
1923
|
+
return plugins;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/plugins/plugin-runner.ts
|
|
1927
|
+
var import_node_fs7 = __toESM(require("fs"), 1);
|
|
1928
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1929
|
+
function isObject(value) {
|
|
1930
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1931
|
+
}
|
|
1932
|
+
function resolvePathValue(input, pathExpr) {
|
|
1933
|
+
const tokens = pathExpr.split(".").map((entry) => entry.trim()).filter(Boolean);
|
|
1934
|
+
if (tokens.length === 0) return void 0;
|
|
1935
|
+
let cursor = input;
|
|
1936
|
+
for (const token of tokens) {
|
|
1937
|
+
if (!isObject(cursor)) return void 0;
|
|
1938
|
+
cursor = cursor[token];
|
|
1939
|
+
}
|
|
1940
|
+
return cursor;
|
|
1941
|
+
}
|
|
1942
|
+
function renderTemplate(template, context) {
|
|
1943
|
+
return template.replace(/{{\s*([^}]+?)\s*}}/g, (_, expr) => {
|
|
1944
|
+
const value = resolvePathValue(context, expr);
|
|
1945
|
+
if (value === null || value === void 0) return "";
|
|
1946
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1947
|
+
return String(value);
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
function resolveSecrets(template, env, plugin) {
|
|
1951
|
+
return template.replace(/\$\{([A-Z0-9_]+)\}/g, (_, secretName) => {
|
|
1952
|
+
if (!plugin.secrets.includes(secretName)) {
|
|
1953
|
+
throw new Error(`Plugin '${plugin.id}' references undeclared secret '${secretName}'.`);
|
|
1954
|
+
}
|
|
1955
|
+
const value = env[secretName];
|
|
1956
|
+
if (!value) {
|
|
1957
|
+
throw new Error(`Plugin '${plugin.id}' missing environment value for '${secretName}'.`);
|
|
1958
|
+
}
|
|
1959
|
+
return value;
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
function filterMatches(filter, eventView) {
|
|
1963
|
+
if (!filter) return true;
|
|
1964
|
+
for (const [key, expected] of Object.entries(filter)) {
|
|
1965
|
+
const actual = resolvePathValue(eventView, key);
|
|
1966
|
+
if (Array.isArray(expected)) {
|
|
1967
|
+
const allowed = expected.map((entry) => String(entry));
|
|
1968
|
+
if (!allowed.includes(String(actual))) return false;
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
if (String(actual) !== String(expected)) return false;
|
|
1972
|
+
}
|
|
1973
|
+
return true;
|
|
1974
|
+
}
|
|
1975
|
+
function eventTemplateView(event) {
|
|
1976
|
+
return {
|
|
1977
|
+
type: event.type,
|
|
1978
|
+
timestamp: event.timestamp,
|
|
1979
|
+
...event.payload
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
function buildContext(event, graph) {
|
|
1983
|
+
const context = {
|
|
1984
|
+
event: eventTemplateView(event)
|
|
1985
|
+
};
|
|
1986
|
+
if (graph) {
|
|
1987
|
+
const payload = event.payload;
|
|
1988
|
+
const taskId = typeof payload.task_id === "string" ? payload.task_id : void 0;
|
|
1989
|
+
const deliveryId = typeof payload.delivery_id === "string" ? payload.delivery_id : void 0;
|
|
1990
|
+
if (taskId) {
|
|
1991
|
+
const task = graph.nodes.get(taskId);
|
|
1992
|
+
if (task) {
|
|
1993
|
+
context.task = task;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
if (deliveryId) {
|
|
1997
|
+
const delivery = graph.deliveries.get(deliveryId);
|
|
1998
|
+
if (delivery) {
|
|
1999
|
+
context.delivery = delivery;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return context;
|
|
2004
|
+
}
|
|
2005
|
+
function appendRunLog(coopDir, record) {
|
|
2006
|
+
const runsDir = import_node_path4.default.join(coopDir, "runs");
|
|
2007
|
+
import_node_fs7.default.mkdirSync(runsDir, { recursive: true });
|
|
2008
|
+
const logPath = import_node_path4.default.join(runsDir, "plugin-events.log");
|
|
2009
|
+
import_node_fs7.default.appendFileSync(logPath, `${JSON.stringify(record)}
|
|
2010
|
+
`, "utf8");
|
|
2011
|
+
}
|
|
2012
|
+
async function executeWebhookAction(plugin, trigger, action, context, env, fetchImpl) {
|
|
2013
|
+
const resolvedUrl = resolveSecrets(action.url, env, plugin);
|
|
2014
|
+
const template = action.template ?? "{{ event.type }}";
|
|
2015
|
+
const body = renderTemplate(resolveSecrets(template, env, plugin), context);
|
|
2016
|
+
const headers = {
|
|
2017
|
+
"content-type": "text/plain; charset=utf-8"
|
|
2018
|
+
};
|
|
2019
|
+
for (const [key, value] of Object.entries(action.headers ?? {})) {
|
|
2020
|
+
headers[key] = resolveSecrets(value, env, plugin);
|
|
2021
|
+
}
|
|
2022
|
+
const response = await fetchImpl(resolvedUrl, {
|
|
2023
|
+
method: "POST",
|
|
2024
|
+
headers,
|
|
2025
|
+
body
|
|
2026
|
+
});
|
|
2027
|
+
return {
|
|
2028
|
+
statusCode: response.status,
|
|
2029
|
+
message: `${trigger.event} -> ${response.status}`
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function executeConsoleAction(plugin, action, context, env) {
|
|
2033
|
+
const template = resolveSecrets(action.template, env, plugin);
|
|
2034
|
+
const message = renderTemplate(template, context);
|
|
2035
|
+
console.log(`[COOP][plugin:${plugin.id}] ${message}`);
|
|
2036
|
+
return { message };
|
|
2037
|
+
}
|
|
2038
|
+
async function executeCustomAction(plugin, trigger, action, context, env, actionHandlers) {
|
|
2039
|
+
const handler = actionHandlers[action.type];
|
|
2040
|
+
if (!handler) {
|
|
2041
|
+
throw new Error(`Plugin action '${action.type}' requires a registered action handler.`);
|
|
2042
|
+
}
|
|
2043
|
+
return handler(plugin, trigger, action, context, env);
|
|
2044
|
+
}
|
|
2045
|
+
async function run_plugins_for_event(coopDir, event, options = {}) {
|
|
2046
|
+
const env = options.env ?? process.env;
|
|
2047
|
+
const fetchImpl = options.fetch_impl ?? fetch;
|
|
2048
|
+
const graph = options.graph;
|
|
2049
|
+
const actionHandlers = options.action_handlers ?? {};
|
|
2050
|
+
const plugins = load_plugins(coopDir);
|
|
2051
|
+
const context = buildContext(event, graph);
|
|
2052
|
+
const eventView = context.event;
|
|
2053
|
+
const results = [];
|
|
2054
|
+
for (const plugin of plugins) {
|
|
2055
|
+
for (const trigger of plugin.triggers) {
|
|
2056
|
+
if (trigger.event !== event.type) continue;
|
|
2057
|
+
if (!filterMatches(trigger.filter, eventView)) continue;
|
|
2058
|
+
const base = {
|
|
2059
|
+
plugin_id: plugin.id,
|
|
2060
|
+
plugin_name: plugin.name,
|
|
2061
|
+
trigger_event: trigger.event,
|
|
2062
|
+
action_type: trigger.action.type,
|
|
2063
|
+
success: false,
|
|
2064
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2065
|
+
};
|
|
2066
|
+
try {
|
|
2067
|
+
if (trigger.action.type === "webhook") {
|
|
2068
|
+
const result = await executeWebhookAction(plugin, trigger, trigger.action, context, env, fetchImpl);
|
|
2069
|
+
base.success = true;
|
|
2070
|
+
base.status_code = result.statusCode;
|
|
2071
|
+
base.message = result.message;
|
|
2072
|
+
} else if (trigger.action.type === "console") {
|
|
2073
|
+
const result = executeConsoleAction(plugin, trigger.action, context, env);
|
|
2074
|
+
base.success = true;
|
|
2075
|
+
base.message = result.message;
|
|
2076
|
+
} else {
|
|
2077
|
+
const result = await executeCustomAction(plugin, trigger, trigger.action, context, env, actionHandlers);
|
|
2078
|
+
base.success = true;
|
|
2079
|
+
base.status_code = result.statusCode;
|
|
2080
|
+
base.message = result.message;
|
|
2081
|
+
}
|
|
2082
|
+
} catch (error6) {
|
|
2083
|
+
base.success = false;
|
|
2084
|
+
base.error = error6 instanceof Error ? error6.message : String(error6);
|
|
2085
|
+
}
|
|
2086
|
+
appendRunLog(coopDir, base);
|
|
2087
|
+
results.push(base);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return results;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// src/models/run.ts
|
|
2094
|
+
var RunStatus = {
|
|
2095
|
+
pending: "pending",
|
|
2096
|
+
running: "running",
|
|
2097
|
+
paused: "paused",
|
|
2098
|
+
completed: "completed",
|
|
2099
|
+
failed: "failed",
|
|
2100
|
+
canceled: "canceled"
|
|
2101
|
+
};
|
|
2102
|
+
var RunStepStatus = {
|
|
2103
|
+
completed: "completed",
|
|
2104
|
+
failed: "failed",
|
|
2105
|
+
paused: "paused",
|
|
2106
|
+
skipped: "skipped"
|
|
2107
|
+
};
|
|
2108
|
+
|
|
2109
|
+
// src/parser/idea-parser.ts
|
|
2110
|
+
var import_node_fs8 = __toESM(require("fs"), 1);
|
|
738
2111
|
function asStringArray2(value) {
|
|
739
2112
|
if (!Array.isArray(value)) {
|
|
740
2113
|
return [];
|
|
@@ -766,6 +2139,7 @@ function parseIdeaFromRaw(raw, source) {
|
|
|
766
2139
|
id,
|
|
767
2140
|
title,
|
|
768
2141
|
created,
|
|
2142
|
+
aliases: asStringArray2(raw.aliases),
|
|
769
2143
|
author,
|
|
770
2144
|
status,
|
|
771
2145
|
tags: asStringArray2(raw.tags),
|
|
@@ -782,12 +2156,12 @@ function parseIdeaContent(content, source = "<content>") {
|
|
|
782
2156
|
};
|
|
783
2157
|
}
|
|
784
2158
|
function parseIdeaFile(filePath) {
|
|
785
|
-
const content =
|
|
2159
|
+
const content = import_node_fs8.default.readFileSync(filePath, "utf8");
|
|
786
2160
|
return parseIdeaContent(content, filePath);
|
|
787
2161
|
}
|
|
788
2162
|
|
|
789
2163
|
// src/parser/task-writer.ts
|
|
790
|
-
var
|
|
2164
|
+
var import_node_fs9 = __toESM(require("fs"), 1);
|
|
791
2165
|
var TASK_FIELD_ORDER = [
|
|
792
2166
|
"id",
|
|
793
2167
|
"title",
|
|
@@ -795,6 +2169,7 @@ var TASK_FIELD_ORDER = [
|
|
|
795
2169
|
"status",
|
|
796
2170
|
"created",
|
|
797
2171
|
"updated",
|
|
2172
|
+
"aliases",
|
|
798
2173
|
"priority",
|
|
799
2174
|
"track",
|
|
800
2175
|
"assignee",
|
|
@@ -832,123 +2207,1971 @@ function buildOrderedFrontmatter(task, raw) {
|
|
|
832
2207
|
if (!raw) {
|
|
833
2208
|
return ordered;
|
|
834
2209
|
}
|
|
835
|
-
for (const key of Object.keys(raw)) {
|
|
836
|
-
if (ORDERED_TASK_FIELDS.has(key)) {
|
|
837
|
-
continue;
|
|
2210
|
+
for (const key of Object.keys(raw)) {
|
|
2211
|
+
if (ORDERED_TASK_FIELDS.has(key)) {
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
ordered[key] = raw[key];
|
|
2215
|
+
}
|
|
2216
|
+
return ordered;
|
|
2217
|
+
}
|
|
2218
|
+
function writeTask(task, options = {}) {
|
|
2219
|
+
const frontmatter = buildOrderedFrontmatter(task, options.raw);
|
|
2220
|
+
const output = stringifyFrontmatter(frontmatter, options.body ?? "");
|
|
2221
|
+
if (options.filePath) {
|
|
2222
|
+
const existedBeforeWrite = import_node_fs9.default.existsSync(options.filePath);
|
|
2223
|
+
import_node_fs9.default.writeFileSync(options.filePath, output, "utf8");
|
|
2224
|
+
if (!existedBeforeWrite) {
|
|
2225
|
+
options.eventEmitter?.emit({
|
|
2226
|
+
type: "task.created",
|
|
2227
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2228
|
+
payload: {
|
|
2229
|
+
task_id: task.id,
|
|
2230
|
+
title: task.title,
|
|
2231
|
+
task_type: task.type,
|
|
2232
|
+
file_path: options.filePath
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return output;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// src/planning/capacity.ts
|
|
2241
|
+
function toDate(value) {
|
|
2242
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
2243
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
2244
|
+
if (Number.isNaN(parsed.valueOf())) {
|
|
2245
|
+
throw new Error(`Invalid date value '${value}'.`);
|
|
2246
|
+
}
|
|
2247
|
+
return parsed;
|
|
2248
|
+
}
|
|
2249
|
+
function isoDate(value) {
|
|
2250
|
+
return value.toISOString().slice(0, 10);
|
|
2251
|
+
}
|
|
2252
|
+
function startOfWeek(date) {
|
|
2253
|
+
const copy = new Date(date.getTime());
|
|
2254
|
+
const day = copy.getUTCDay();
|
|
2255
|
+
const offset = day === 0 ? -6 : 1 - day;
|
|
2256
|
+
copy.setUTCDate(copy.getUTCDate() + offset);
|
|
2257
|
+
copy.setUTCHours(0, 0, 0, 0);
|
|
2258
|
+
return copy;
|
|
2259
|
+
}
|
|
2260
|
+
function endOfWeek(date) {
|
|
2261
|
+
const start = startOfWeek(date);
|
|
2262
|
+
const end = new Date(start.getTime());
|
|
2263
|
+
end.setUTCDate(end.getUTCDate() + 6);
|
|
2264
|
+
end.setUTCHours(0, 0, 0, 0);
|
|
2265
|
+
return end;
|
|
2266
|
+
}
|
|
2267
|
+
function addDays(date, days) {
|
|
2268
|
+
const next = new Date(date.getTime());
|
|
2269
|
+
next.setUTCDate(next.getUTCDate() + days);
|
|
2270
|
+
return next;
|
|
2271
|
+
}
|
|
2272
|
+
function isBusinessDay(date) {
|
|
2273
|
+
const day = date.getUTCDay();
|
|
2274
|
+
return day >= 1 && day <= 5;
|
|
2275
|
+
}
|
|
2276
|
+
function overlapBusinessDays(from, to, weekStart, weekEnd) {
|
|
2277
|
+
const start = from > weekStart ? from : weekStart;
|
|
2278
|
+
const end = to < weekEnd ? to : weekEnd;
|
|
2279
|
+
if (start > end) return 0;
|
|
2280
|
+
let count = 0;
|
|
2281
|
+
const cursor = new Date(start.getTime());
|
|
2282
|
+
while (cursor <= end) {
|
|
2283
|
+
if (isBusinessDay(cursor)) {
|
|
2284
|
+
count += 1;
|
|
2285
|
+
}
|
|
2286
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
2287
|
+
}
|
|
2288
|
+
return count;
|
|
2289
|
+
}
|
|
2290
|
+
function clampPercent(input) {
|
|
2291
|
+
if (!Number.isFinite(input)) return 0;
|
|
2292
|
+
return Math.min(100, Math.max(0, Number(input)));
|
|
2293
|
+
}
|
|
2294
|
+
function normalizeTrackKey(input) {
|
|
2295
|
+
return input.trim().toLowerCase();
|
|
2296
|
+
}
|
|
2297
|
+
function normalizeAgentKey(input) {
|
|
2298
|
+
return input.trim().toLowerCase();
|
|
2299
|
+
}
|
|
2300
|
+
function simplifiedTrackKeyFromProfile(profileId) {
|
|
2301
|
+
const lower = profileId.toLowerCase();
|
|
2302
|
+
if (lower.endsWith("_team")) return lower.slice(0, -5);
|
|
2303
|
+
if (lower.endsWith("-team")) return lower.slice(0, -5);
|
|
2304
|
+
if (lower.endsWith("_cluster")) return lower.slice(0, -8);
|
|
2305
|
+
if (lower.endsWith("-cluster")) return lower.slice(0, -8);
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
function valuesOfResources(resources) {
|
|
2309
|
+
if (Array.isArray(resources)) return resources;
|
|
2310
|
+
return [...resources.values()];
|
|
2311
|
+
}
|
|
2312
|
+
function selectedProfiles(delivery, resources) {
|
|
2313
|
+
if (!delivery.capacity_profiles || delivery.capacity_profiles.length === 0) {
|
|
2314
|
+
return resources;
|
|
2315
|
+
}
|
|
2316
|
+
const wanted = new Set(delivery.capacity_profiles.map((id) => id.toLowerCase()));
|
|
2317
|
+
return resources.filter((profile) => wanted.has(profile.id.toLowerCase()));
|
|
2318
|
+
}
|
|
2319
|
+
function selectedHumanProfiles(delivery, resources) {
|
|
2320
|
+
return selectedProfiles(delivery, resources).filter((profile) => profile.type === "human");
|
|
2321
|
+
}
|
|
2322
|
+
function dailyHours(memberHoursPerWeek) {
|
|
2323
|
+
return memberHoursPerWeek / 5;
|
|
2324
|
+
}
|
|
2325
|
+
function effective_weekly_hours(profile, week_start, week_end) {
|
|
2326
|
+
const weekStart = toDate(week_start);
|
|
2327
|
+
const weekEnd = toDate(week_end);
|
|
2328
|
+
let gross = 0;
|
|
2329
|
+
for (const member of profile.members) {
|
|
2330
|
+
const baseHours = member.hours_per_week ?? profile.defaults?.hours_per_week ?? 0;
|
|
2331
|
+
let available = baseHours;
|
|
2332
|
+
for (const window of member.availability ?? []) {
|
|
2333
|
+
const from = toDate(window.from);
|
|
2334
|
+
const to = toDate(window.to);
|
|
2335
|
+
const overlapDays = overlapBusinessDays(from, to, weekStart, weekEnd);
|
|
2336
|
+
if (overlapDays <= 0) continue;
|
|
2337
|
+
const unavailablePerDay = Math.max(0, dailyHours(baseHours) - Math.max(0, window.hours_per_day));
|
|
2338
|
+
available -= overlapDays * unavailablePerDay;
|
|
2339
|
+
}
|
|
2340
|
+
gross += Math.max(0, available);
|
|
2341
|
+
}
|
|
2342
|
+
const meetings = clampPercent(profile.overhead?.meetings_percent);
|
|
2343
|
+
const contextSwitch = clampPercent(profile.overhead?.context_switch_percent);
|
|
2344
|
+
const meetingFactor = 1 - meetings / 100;
|
|
2345
|
+
const contextFactor = 1 - contextSwitch / 100;
|
|
2346
|
+
return gross * meetingFactor * contextFactor;
|
|
2347
|
+
}
|
|
2348
|
+
function effective_ai_tokens_per_day(profile) {
|
|
2349
|
+
if (profile.type !== "ai") return 0;
|
|
2350
|
+
return profile.agents.reduce((sum, agent) => sum + Math.max(0, agent.token_limit_per_day ?? 0), 0);
|
|
2351
|
+
}
|
|
2352
|
+
function ai_tokens_per_agent(profile) {
|
|
2353
|
+
if (profile.type !== "ai") return [];
|
|
2354
|
+
return profile.agents.map((agent) => ({
|
|
2355
|
+
id: normalizeAgentKey(agent.id),
|
|
2356
|
+
limit: Math.max(0, agent.token_limit_per_day ?? 0)
|
|
2357
|
+
}));
|
|
2358
|
+
}
|
|
2359
|
+
function addSlot(slots, trackId, week, hours) {
|
|
2360
|
+
const key = normalizeTrackKey(trackId);
|
|
2361
|
+
const byWeek = slots.get(key) ?? /* @__PURE__ */ new Map();
|
|
2362
|
+
byWeek.set(week, (byWeek.get(week) ?? 0) + hours);
|
|
2363
|
+
slots.set(key, byWeek);
|
|
2364
|
+
}
|
|
2365
|
+
function build_capacity_ledger(delivery, resources, today) {
|
|
2366
|
+
const allResources = valuesOfResources(resources);
|
|
2367
|
+
const start = startOfWeek(toDate(today));
|
|
2368
|
+
const target = delivery.target_date ? toDate(delivery.target_date) : addDays(start, 7 * 8);
|
|
2369
|
+
const finalWeekStart = startOfWeek(target);
|
|
2370
|
+
const weeks = /* @__PURE__ */ new Map();
|
|
2371
|
+
const slots = /* @__PURE__ */ new Map();
|
|
2372
|
+
const ai_tokens = /* @__PURE__ */ new Map();
|
|
2373
|
+
const ai_tokens_by_agent = /* @__PURE__ */ new Map();
|
|
2374
|
+
const ai_tokens_consumed_by_agent = /* @__PURE__ */ new Map();
|
|
2375
|
+
const humans = selectedHumanProfiles(delivery, allResources);
|
|
2376
|
+
const selected = selectedProfiles(delivery, allResources);
|
|
2377
|
+
let weekIndex = 0;
|
|
2378
|
+
for (let cursor = new Date(start.getTime()); cursor <= finalWeekStart; cursor = addDays(cursor, 7), weekIndex += 1) {
|
|
2379
|
+
const weekStart = new Date(cursor.getTime());
|
|
2380
|
+
const weekEnd = endOfWeek(weekStart);
|
|
2381
|
+
weeks.set(weekIndex, { start: isoDate(weekStart), end: isoDate(weekEnd) });
|
|
2382
|
+
const totalHours = humans.reduce((sum, profile) => sum + effective_weekly_hours(profile, weekStart, weekEnd), 0);
|
|
2383
|
+
addSlot(slots, "unassigned", weekIndex, totalHours);
|
|
2384
|
+
for (const profile of humans) {
|
|
2385
|
+
const hours = effective_weekly_hours(profile, weekStart, weekEnd);
|
|
2386
|
+
addSlot(slots, profile.id, weekIndex, hours);
|
|
2387
|
+
const simplified = simplifiedTrackKeyFromProfile(profile.id);
|
|
2388
|
+
if (simplified) {
|
|
2389
|
+
addSlot(slots, simplified, weekIndex, hours);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
const aiProfiles = selected.filter((profile) => profile.type === "ai");
|
|
2394
|
+
const defaultTokensPerDay = aiProfiles.reduce((sum, profile) => sum + effective_ai_tokens_per_day(profile), 0);
|
|
2395
|
+
const tokenWindowEnd = delivery.target_date ? toDate(delivery.target_date) : addDays(start, 7 * 8);
|
|
2396
|
+
for (let day = new Date(start.getTime()); day <= tokenWindowEnd; day = addDays(day, 1)) {
|
|
2397
|
+
const dayKey = isoDate(day);
|
|
2398
|
+
ai_tokens.set(dayKey, defaultTokensPerDay);
|
|
2399
|
+
for (const profile of aiProfiles) {
|
|
2400
|
+
for (const agent of ai_tokens_per_agent(profile)) {
|
|
2401
|
+
const byDay = ai_tokens_by_agent.get(agent.id) ?? /* @__PURE__ */ new Map();
|
|
2402
|
+
byDay.set(dayKey, (byDay.get(dayKey) ?? 0) + agent.limit);
|
|
2403
|
+
ai_tokens_by_agent.set(agent.id, byDay);
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
return {
|
|
2408
|
+
slots,
|
|
2409
|
+
weeks,
|
|
2410
|
+
ai_tokens,
|
|
2411
|
+
ai_tokens_by_agent,
|
|
2412
|
+
ai_tokens_consumed_by_agent
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
function findTrackSlots(ledger, trackId) {
|
|
2416
|
+
const normalized = normalizeTrackKey(trackId ?? "unassigned");
|
|
2417
|
+
return ledger.slots.get(normalized) ?? ledger.slots.get("unassigned") ?? null;
|
|
2418
|
+
}
|
|
2419
|
+
function allocate(ledger, task, start_week) {
|
|
2420
|
+
const trackSlots = findTrackSlots(ledger, task.track);
|
|
2421
|
+
if (!trackSlots) {
|
|
2422
|
+
return { success: false, reason: "capacity_exhausted" };
|
|
2423
|
+
}
|
|
2424
|
+
let hoursRemaining = task_effort_hours(task);
|
|
2425
|
+
let week = start_week;
|
|
2426
|
+
while (hoursRemaining > 0) {
|
|
2427
|
+
const available = trackSlots.get(week);
|
|
2428
|
+
if (available == null) {
|
|
2429
|
+
return { success: false, reason: "capacity_exhausted" };
|
|
2430
|
+
}
|
|
2431
|
+
const take = Math.min(hoursRemaining, available);
|
|
2432
|
+
trackSlots.set(week, available - take);
|
|
2433
|
+
hoursRemaining -= take;
|
|
2434
|
+
if (hoursRemaining > 0) {
|
|
2435
|
+
week += 1;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
return {
|
|
2439
|
+
success: true,
|
|
2440
|
+
start_week,
|
|
2441
|
+
end_week: week,
|
|
2442
|
+
duration_weeks: week - start_week + 1
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
function check_wip(track, graph) {
|
|
2446
|
+
if (!track) return true;
|
|
2447
|
+
const limit = track.constraints?.max_concurrent_tasks;
|
|
2448
|
+
if (!Number.isFinite(limit) || Number(limit) <= 0) return true;
|
|
2449
|
+
let inProgressCount = 0;
|
|
2450
|
+
for (const task of graph.nodes.values()) {
|
|
2451
|
+
if ((task.track ?? "unassigned") !== track.id) continue;
|
|
2452
|
+
if (task.status === "in_progress" || task.status === "in_review") {
|
|
2453
|
+
inProgressCount += 1;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return inProgressCount < Number(limit);
|
|
2457
|
+
}
|
|
2458
|
+
function allocate_ai(ledger, task, day, fallback_tokens = 5e4) {
|
|
2459
|
+
const tokensNeeded = task.resources?.ai_tokens ?? task.execution?.constraints?.max_tokens ?? fallback_tokens;
|
|
2460
|
+
const preferredAgent = task.execution?.agent?.trim().toLowerCase();
|
|
2461
|
+
const dayKey = day;
|
|
2462
|
+
if (preferredAgent && ledger.ai_tokens_by_agent.size > 0) {
|
|
2463
|
+
const allocated = allocate_ai_tokens(ledger, preferredAgent, tokensNeeded, dayKey);
|
|
2464
|
+
if (allocated) {
|
|
2465
|
+
return { success: true };
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (ledger.ai_tokens_by_agent.size > 0) {
|
|
2469
|
+
const candidates = Array.from(ledger.ai_tokens_by_agent.keys()).sort((a, b) => a.localeCompare(b));
|
|
2470
|
+
for (const agentId of candidates) {
|
|
2471
|
+
if (preferredAgent && agentId === preferredAgent) continue;
|
|
2472
|
+
const allocated = allocate_ai_tokens(ledger, agentId, tokensNeeded, dayKey);
|
|
2473
|
+
if (allocated) {
|
|
2474
|
+
return { success: true };
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
const available = ledger.ai_tokens.get(dayKey) ?? 0;
|
|
2479
|
+
if (tokensNeeded > available) {
|
|
2480
|
+
return { success: false, reason: "ai_capacity_exhausted" };
|
|
2481
|
+
}
|
|
2482
|
+
ledger.ai_tokens.set(dayKey, available - tokensNeeded);
|
|
2483
|
+
return { success: true };
|
|
2484
|
+
}
|
|
2485
|
+
function allocate_ai_tokens(ledger, agent, tokens, day) {
|
|
2486
|
+
const normalizedAgent = normalizeAgentKey(agent);
|
|
2487
|
+
if (tokens <= 0) return true;
|
|
2488
|
+
const byDay = ledger.ai_tokens_by_agent.get(normalizedAgent);
|
|
2489
|
+
if (!byDay) return false;
|
|
2490
|
+
const available = byDay.get(day) ?? 0;
|
|
2491
|
+
if (tokens > available) return false;
|
|
2492
|
+
byDay.set(day, available - tokens);
|
|
2493
|
+
ledger.ai_tokens_by_agent.set(normalizedAgent, byDay);
|
|
2494
|
+
const consumed = ledger.ai_tokens_consumed_by_agent.get(normalizedAgent) ?? /* @__PURE__ */ new Map();
|
|
2495
|
+
consumed.set(day, (consumed.get(day) ?? 0) + tokens);
|
|
2496
|
+
ledger.ai_tokens_consumed_by_agent.set(normalizedAgent, consumed);
|
|
2497
|
+
const totalAvailable = ledger.ai_tokens.get(day) ?? 0;
|
|
2498
|
+
ledger.ai_tokens.set(day, Math.max(0, totalAvailable - tokens));
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
function get_remaining_tokens(ledger, agent, day) {
|
|
2502
|
+
const normalizedAgent = normalizeAgentKey(agent);
|
|
2503
|
+
const byDay = ledger.ai_tokens_by_agent.get(normalizedAgent);
|
|
2504
|
+
if (!byDay) return 0;
|
|
2505
|
+
return byDay.get(day) ?? 0;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// src/planning/simulator.ts
|
|
2509
|
+
var import_node_fs10 = __toESM(require("fs"), 1);
|
|
2510
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
2511
|
+
var import_node_worker_threads = require("worker_threads");
|
|
2512
|
+
var import_node_url = require("url");
|
|
2513
|
+
|
|
2514
|
+
// src/planning/sampling.ts
|
|
2515
|
+
var DEFAULT_CONFIG2 = {
|
|
2516
|
+
version: 2,
|
|
2517
|
+
project: {
|
|
2518
|
+
name: "COOP",
|
|
2519
|
+
id: "coop"
|
|
2520
|
+
},
|
|
2521
|
+
defaults: {
|
|
2522
|
+
task: {
|
|
2523
|
+
complexity: "medium"
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2527
|
+
function sample_standard_normal(rng) {
|
|
2528
|
+
let u = 0;
|
|
2529
|
+
let v = 0;
|
|
2530
|
+
while (u === 0) u = rng();
|
|
2531
|
+
while (v === 0) v = rng();
|
|
2532
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
|
|
2533
|
+
}
|
|
2534
|
+
function sample_gamma(shape, rng) {
|
|
2535
|
+
if (shape <= 0) {
|
|
2536
|
+
throw new Error("Gamma shape must be > 0.");
|
|
2537
|
+
}
|
|
2538
|
+
if (shape < 1) {
|
|
2539
|
+
const u = rng();
|
|
2540
|
+
return sample_gamma(shape + 1, rng) * Math.pow(u, 1 / shape);
|
|
2541
|
+
}
|
|
2542
|
+
const d = shape - 1 / 3;
|
|
2543
|
+
const c = 1 / Math.sqrt(9 * d);
|
|
2544
|
+
while (true) {
|
|
2545
|
+
const x = sample_standard_normal(rng);
|
|
2546
|
+
const v = Math.pow(1 + c * x, 3);
|
|
2547
|
+
if (v <= 0) continue;
|
|
2548
|
+
const u = rng();
|
|
2549
|
+
if (u < 1 - 0.0331 * Math.pow(x, 4)) {
|
|
2550
|
+
return d * v;
|
|
2551
|
+
}
|
|
2552
|
+
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) {
|
|
2553
|
+
return d * v;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
function sample_beta(alpha, beta, rng) {
|
|
2558
|
+
const x = sample_gamma(alpha, rng);
|
|
2559
|
+
const y = sample_gamma(beta, rng);
|
|
2560
|
+
return x / (x + y);
|
|
2561
|
+
}
|
|
2562
|
+
function sample_pert_beta(optimistic, expected, pessimistic, rng = Math.random) {
|
|
2563
|
+
if (!Number.isFinite(optimistic) || !Number.isFinite(expected) || !Number.isFinite(pessimistic)) {
|
|
2564
|
+
throw new Error("PERT inputs must be finite numbers.");
|
|
2565
|
+
}
|
|
2566
|
+
if (optimistic <= 0 || expected <= 0 || pessimistic <= 0) {
|
|
2567
|
+
throw new Error("PERT inputs must be positive.");
|
|
2568
|
+
}
|
|
2569
|
+
if (optimistic > pessimistic) {
|
|
2570
|
+
throw new Error("PERT optimistic must be <= pessimistic.");
|
|
2571
|
+
}
|
|
2572
|
+
if (optimistic === pessimistic) {
|
|
2573
|
+
return optimistic;
|
|
2574
|
+
}
|
|
2575
|
+
const expectedClamped = Math.min(pessimistic, Math.max(optimistic, expected));
|
|
2576
|
+
const range = pessimistic - optimistic;
|
|
2577
|
+
const lambda = 4;
|
|
2578
|
+
const alpha = 1 + lambda * ((expectedClamped - optimistic) / range);
|
|
2579
|
+
const beta = 1 + lambda * ((pessimistic - expectedClamped) / range);
|
|
2580
|
+
const sample = sample_beta(alpha, beta, rng);
|
|
2581
|
+
return optimistic + sample * range;
|
|
2582
|
+
}
|
|
2583
|
+
function sample_task_hours(task, rng = Math.random) {
|
|
2584
|
+
if (task.estimate) {
|
|
2585
|
+
return sample_pert_beta(
|
|
2586
|
+
task.estimate.optimistic_hours,
|
|
2587
|
+
task.estimate.expected_hours,
|
|
2588
|
+
task.estimate.pessimistic_hours,
|
|
2589
|
+
rng
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
const base = effort_or_default(task, DEFAULT_CONFIG2);
|
|
2593
|
+
const min = base * 0.7;
|
|
2594
|
+
const max = base * 1.3;
|
|
2595
|
+
return min + (max - min) * rng();
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// src/planning/simulator.ts
|
|
2599
|
+
var DEFAULT_COMPLEXITY = "medium";
|
|
2600
|
+
var DEFAULT_MONTE_CARLO_WORKERS = 4;
|
|
2601
|
+
function asDate(value) {
|
|
2602
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
2603
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
2604
|
+
if (Number.isNaN(parsed.valueOf())) {
|
|
2605
|
+
throw new Error(`Invalid date '${value}'.`);
|
|
2606
|
+
}
|
|
2607
|
+
return parsed;
|
|
2608
|
+
}
|
|
2609
|
+
function isoDate2(value) {
|
|
2610
|
+
return value.toISOString().slice(0, 10);
|
|
2611
|
+
}
|
|
2612
|
+
function startOfWeek2(date) {
|
|
2613
|
+
const copy = new Date(date.getTime());
|
|
2614
|
+
const day = copy.getUTCDay();
|
|
2615
|
+
const offset = day === 0 ? -6 : 1 - day;
|
|
2616
|
+
copy.setUTCDate(copy.getUTCDate() + offset);
|
|
2617
|
+
copy.setUTCHours(0, 0, 0, 0);
|
|
2618
|
+
return copy;
|
|
2619
|
+
}
|
|
2620
|
+
function addDays2(date, days) {
|
|
2621
|
+
const out = new Date(date.getTime());
|
|
2622
|
+
out.setUTCDate(out.getUTCDate() + days);
|
|
2623
|
+
return out;
|
|
2624
|
+
}
|
|
2625
|
+
function addWeeks(date, weeks) {
|
|
2626
|
+
return addDays2(date, weeks * 7);
|
|
2627
|
+
}
|
|
2628
|
+
function week_to_date(today, week) {
|
|
2629
|
+
const origin = startOfWeek2(asDate(today));
|
|
2630
|
+
const date = addDays2(origin, week * 7 + 6);
|
|
2631
|
+
return isoDate2(date);
|
|
2632
|
+
}
|
|
2633
|
+
function clone_ledger(ledger) {
|
|
2634
|
+
return {
|
|
2635
|
+
slots: new Map(
|
|
2636
|
+
Array.from(ledger.slots.entries(), ([track, byWeek]) => [track, new Map(byWeek)])
|
|
2637
|
+
),
|
|
2638
|
+
weeks: new Map(ledger.weeks),
|
|
2639
|
+
ai_tokens: new Map(ledger.ai_tokens),
|
|
2640
|
+
ai_tokens_by_agent: new Map(
|
|
2641
|
+
Array.from(ledger.ai_tokens_by_agent.entries(), ([agent, byDay]) => [agent, new Map(byDay)])
|
|
2642
|
+
),
|
|
2643
|
+
ai_tokens_consumed_by_agent: new Map(
|
|
2644
|
+
Array.from(ledger.ai_tokens_consumed_by_agent.entries(), ([agent, byDay]) => [
|
|
2645
|
+
agent,
|
|
2646
|
+
new Map(byDay)
|
|
2647
|
+
])
|
|
2648
|
+
)
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
function normalize_track(track) {
|
|
2652
|
+
return (track ?? "unassigned").trim().toLowerCase();
|
|
2653
|
+
}
|
|
2654
|
+
function find_track_slots(ledger, track) {
|
|
2655
|
+
const key = normalize_track(track);
|
|
2656
|
+
return ledger.slots.get(key) ?? ledger.slots.get("unassigned") ?? null;
|
|
2657
|
+
}
|
|
2658
|
+
function max_week(ledger) {
|
|
2659
|
+
const keys = Array.from(ledger.weeks.keys());
|
|
2660
|
+
if (keys.length === 0) return 0;
|
|
2661
|
+
return Math.max(...keys);
|
|
2662
|
+
}
|
|
2663
|
+
function track_limit(trackId, graph) {
|
|
2664
|
+
const limit = graph.tracks.get(trackId)?.constraints?.max_concurrent_tasks;
|
|
2665
|
+
if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) {
|
|
2666
|
+
return Number.POSITIVE_INFINITY;
|
|
2667
|
+
}
|
|
2668
|
+
return limit;
|
|
2669
|
+
}
|
|
2670
|
+
function ordered_tasks(tasks, graph) {
|
|
2671
|
+
const wanted = new Map(tasks.map((task) => [task.id, task]));
|
|
2672
|
+
const ordered = [];
|
|
2673
|
+
for (const id of graph.topological_order) {
|
|
2674
|
+
const task = wanted.get(id);
|
|
2675
|
+
if (task) {
|
|
2676
|
+
ordered.push(task);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
if (ordered.length !== wanted.size) {
|
|
2680
|
+
const missing = Array.from(wanted.keys()).filter((id) => !ordered.some((task) => task.id === id));
|
|
2681
|
+
throw new Error(`Tasks missing from graph topological order: ${missing.join(", ")}.`);
|
|
2682
|
+
}
|
|
2683
|
+
return ordered;
|
|
2684
|
+
}
|
|
2685
|
+
function task_with_default_complexity(task) {
|
|
2686
|
+
const hasHumanHours = typeof task.resources?.human_hours === "number";
|
|
2687
|
+
if (hasHumanHours || task.estimate || task.complexity) return task;
|
|
2688
|
+
return {
|
|
2689
|
+
...task,
|
|
2690
|
+
complexity: DEFAULT_COMPLEXITY
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
function sum_hours(slots) {
|
|
2694
|
+
if (!slots) return 0;
|
|
2695
|
+
let total = 0;
|
|
2696
|
+
for (const value of slots.values()) {
|
|
2697
|
+
total += value;
|
|
2698
|
+
}
|
|
2699
|
+
return total;
|
|
2700
|
+
}
|
|
2701
|
+
function compute_utilization(initial, current, tracks) {
|
|
2702
|
+
const utilization = [];
|
|
2703
|
+
for (const track of tracks) {
|
|
2704
|
+
const normalized = normalize_track(track);
|
|
2705
|
+
const initialSlots = find_track_slots(initial, normalized);
|
|
2706
|
+
const currentSlots = find_track_slots(current, normalized);
|
|
2707
|
+
const capacity = sum_hours(initialSlots ?? void 0);
|
|
2708
|
+
const remaining = sum_hours(currentSlots ?? void 0);
|
|
2709
|
+
const allocated = Math.max(0, capacity - remaining);
|
|
2710
|
+
utilization.push({
|
|
2711
|
+
track: normalized,
|
|
2712
|
+
allocated_hours: allocated,
|
|
2713
|
+
capacity_hours: capacity,
|
|
2714
|
+
utilization: capacity > 0 ? allocated / capacity : 0
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
utilization.sort((a, b) => a.track.localeCompare(b.track));
|
|
2718
|
+
return utilization;
|
|
2719
|
+
}
|
|
2720
|
+
function plan_allocation(trackSlots, effort, start_week) {
|
|
2721
|
+
let hoursRemaining = effort;
|
|
2722
|
+
let week = start_week;
|
|
2723
|
+
while (hoursRemaining > 0) {
|
|
2724
|
+
const available = trackSlots.get(week);
|
|
2725
|
+
if (available == null || available <= 0) {
|
|
2726
|
+
return { success: false, reason: "capacity_exhausted" };
|
|
2727
|
+
}
|
|
2728
|
+
hoursRemaining -= Math.min(hoursRemaining, available);
|
|
2729
|
+
if (hoursRemaining > 0) {
|
|
2730
|
+
week += 1;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
return {
|
|
2734
|
+
success: true,
|
|
2735
|
+
start_week,
|
|
2736
|
+
end_week: week,
|
|
2737
|
+
duration_weeks: week - start_week + 1
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
function apply_allocation(trackSlots, effort, allocation) {
|
|
2741
|
+
let hoursRemaining = effort;
|
|
2742
|
+
for (let week = allocation.start_week; week <= allocation.end_week && hoursRemaining > 0; week += 1) {
|
|
2743
|
+
const available = trackSlots.get(week) ?? 0;
|
|
2744
|
+
const take = Math.min(hoursRemaining, available);
|
|
2745
|
+
trackSlots.set(week, available - take);
|
|
2746
|
+
hoursRemaining -= take;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
function simulate_ordered_schedule(ordered, includedTaskIds, graph, delivery, today) {
|
|
2750
|
+
const ledger = build_capacity_ledger(delivery, graph.resources, today);
|
|
2751
|
+
const initialLedger = clone_ledger(ledger);
|
|
2752
|
+
const schedule = {};
|
|
2753
|
+
const wip = /* @__PURE__ */ new Map();
|
|
2754
|
+
const maxWeek = max_week(ledger);
|
|
2755
|
+
const tracks = /* @__PURE__ */ new Set();
|
|
2756
|
+
for (const rawTask of ordered) {
|
|
2757
|
+
const task = task_with_default_complexity(rawTask);
|
|
2758
|
+
const track = normalize_track(task.track);
|
|
2759
|
+
tracks.add(track);
|
|
2760
|
+
if (task.status === "done" || task.status === "canceled") {
|
|
2761
|
+
schedule[task.id] = {
|
|
2762
|
+
task_id: task.id,
|
|
2763
|
+
track,
|
|
2764
|
+
start_week: null,
|
|
2765
|
+
end_week: null,
|
|
2766
|
+
already_complete: true
|
|
2767
|
+
};
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
let earliest = 0;
|
|
2771
|
+
for (const depId of task.depends_on ?? []) {
|
|
2772
|
+
const depSchedule = schedule[depId];
|
|
2773
|
+
if (depSchedule && typeof depSchedule.end_week === "number") {
|
|
2774
|
+
earliest = Math.max(earliest, depSchedule.end_week + 1);
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
const depTask = graph.nodes.get(depId);
|
|
2778
|
+
if (depTask && depTask.status !== "done" && depTask.status !== "canceled" && !includedTaskIds.has(depId)) {
|
|
2779
|
+
return {
|
|
2780
|
+
schedule,
|
|
2781
|
+
projected_completion: null,
|
|
2782
|
+
total_weeks: 0,
|
|
2783
|
+
utilization_by_track: compute_utilization(initialLedger, ledger, /* @__PURE__ */ new Set([track])),
|
|
2784
|
+
error: {
|
|
2785
|
+
code: "schedule_overflow",
|
|
2786
|
+
task: task.id
|
|
2787
|
+
}
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const trackSlots = find_track_slots(ledger, track);
|
|
2792
|
+
const trackWip = wip.get(track) ?? /* @__PURE__ */ new Map();
|
|
2793
|
+
const limit = track_limit(track, graph);
|
|
2794
|
+
const effort = task_effort_hours(task);
|
|
2795
|
+
let placed = false;
|
|
2796
|
+
for (let week = earliest; week <= maxWeek; week += 1) {
|
|
2797
|
+
const hasCapacity = (trackSlots?.get(week) ?? 0) > 0;
|
|
2798
|
+
const hasWipRoom = (trackWip.get(week) ?? 0) < limit;
|
|
2799
|
+
if (!hasCapacity || !hasWipRoom || !trackSlots) continue;
|
|
2800
|
+
const allocation = plan_allocation(trackSlots, effort, week);
|
|
2801
|
+
if (!allocation.success || allocation.start_week == null || allocation.end_week == null) {
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
let violatesWip = false;
|
|
2805
|
+
for (let w = allocation.start_week; w <= allocation.end_week; w += 1) {
|
|
2806
|
+
const nextWip = (trackWip.get(w) ?? 0) + 1;
|
|
2807
|
+
if (nextWip > limit) {
|
|
2808
|
+
violatesWip = true;
|
|
2809
|
+
break;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
if (violatesWip) continue;
|
|
2813
|
+
apply_allocation(trackSlots, effort, {
|
|
2814
|
+
start_week: allocation.start_week,
|
|
2815
|
+
end_week: allocation.end_week
|
|
2816
|
+
});
|
|
2817
|
+
schedule[task.id] = {
|
|
2818
|
+
task_id: task.id,
|
|
2819
|
+
track,
|
|
2820
|
+
start_week: allocation.start_week,
|
|
2821
|
+
end_week: allocation.end_week
|
|
2822
|
+
};
|
|
2823
|
+
for (let w = allocation.start_week; w <= allocation.end_week; w += 1) {
|
|
2824
|
+
trackWip.set(w, (trackWip.get(w) ?? 0) + 1);
|
|
2825
|
+
}
|
|
2826
|
+
wip.set(track, trackWip);
|
|
2827
|
+
placed = true;
|
|
2828
|
+
break;
|
|
2829
|
+
}
|
|
2830
|
+
if (!placed) {
|
|
2831
|
+
return {
|
|
2832
|
+
schedule,
|
|
2833
|
+
projected_completion: null,
|
|
2834
|
+
total_weeks: 0,
|
|
2835
|
+
utilization_by_track: compute_utilization(initialLedger, ledger, /* @__PURE__ */ new Set([track])),
|
|
2836
|
+
error: {
|
|
2837
|
+
code: "schedule_overflow",
|
|
2838
|
+
task: task.id
|
|
2839
|
+
}
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
let latestEnd = -1;
|
|
2844
|
+
for (const entry of Object.values(schedule)) {
|
|
2845
|
+
if (entry.already_complete) continue;
|
|
2846
|
+
if (typeof entry.end_week === "number") {
|
|
2847
|
+
latestEnd = Math.max(latestEnd, entry.end_week);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
return {
|
|
2851
|
+
schedule,
|
|
2852
|
+
projected_completion: latestEnd >= 0 ? week_to_date(today, latestEnd) : isoDate2(asDate(today)),
|
|
2853
|
+
total_weeks: latestEnd >= 0 ? latestEnd + 1 : 0,
|
|
2854
|
+
utilization_by_track: compute_utilization(initialLedger, ledger, tracks)
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
function simulate_schedule(tasks, graph, delivery, today) {
|
|
2858
|
+
const ordered = ordered_tasks(tasks, graph);
|
|
2859
|
+
return simulate_ordered_schedule(ordered, new Set(ordered.map((task) => task.id)), graph, delivery, today);
|
|
2860
|
+
}
|
|
2861
|
+
function percentile(sorted, ratio) {
|
|
2862
|
+
if (sorted.length === 0) return Number.NaN;
|
|
2863
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.floor((sorted.length - 1) * ratio)));
|
|
2864
|
+
return sorted[index] ?? Number.NaN;
|
|
2865
|
+
}
|
|
2866
|
+
function mean(values) {
|
|
2867
|
+
if (values.length === 0) return Number.NaN;
|
|
2868
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
2869
|
+
}
|
|
2870
|
+
function stddev(values, avg) {
|
|
2871
|
+
if (values.length === 0) return Number.NaN;
|
|
2872
|
+
const variance = values.reduce((sum, value) => sum + Math.pow(value - avg, 2), 0) / values.length;
|
|
2873
|
+
return Math.sqrt(variance);
|
|
2874
|
+
}
|
|
2875
|
+
function toDayOffset(origin, value) {
|
|
2876
|
+
const target = asDate(value);
|
|
2877
|
+
return Math.round((target.getTime() - origin.getTime()) / (24 * 60 * 60 * 1e3));
|
|
2878
|
+
}
|
|
2879
|
+
function fromDayOffset(origin, offset) {
|
|
2880
|
+
return isoDate2(addDays2(origin, offset));
|
|
2881
|
+
}
|
|
2882
|
+
function resolve_scoped_tasks(delivery, graph) {
|
|
2883
|
+
const include = new Set(delivery.scope.include);
|
|
2884
|
+
for (const excluded of delivery.scope.exclude) {
|
|
2885
|
+
include.delete(excluded);
|
|
2886
|
+
}
|
|
2887
|
+
const ordered = [];
|
|
2888
|
+
for (const taskId of graph.topological_order) {
|
|
2889
|
+
if (!include.has(taskId)) continue;
|
|
2890
|
+
const task = graph.nodes.get(taskId);
|
|
2891
|
+
if (!task) continue;
|
|
2892
|
+
if (task.status === "done" || task.status === "canceled") continue;
|
|
2893
|
+
ordered.push(task);
|
|
2894
|
+
}
|
|
2895
|
+
return ordered;
|
|
2896
|
+
}
|
|
2897
|
+
function sample_ordered_tasks(tasks, rng) {
|
|
2898
|
+
const sampled = new Array(tasks.length);
|
|
2899
|
+
for (let i = 0; i < tasks.length; i += 1) {
|
|
2900
|
+
const task = tasks[i];
|
|
2901
|
+
sampled[i] = {
|
|
2902
|
+
...task,
|
|
2903
|
+
resources: {
|
|
2904
|
+
...task.resources ?? {},
|
|
2905
|
+
human_hours: sample_task_hours(task, rng)
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
return sampled;
|
|
2910
|
+
}
|
|
2911
|
+
function create_seeded_rng(seed) {
|
|
2912
|
+
let state = seed >>> 0 || 1831565813;
|
|
2913
|
+
return () => {
|
|
2914
|
+
state = state + 1831565813 | 0;
|
|
2915
|
+
let t = Math.imul(state ^ state >>> 15, 1 | state);
|
|
2916
|
+
t ^= t + Math.imul(t ^ t >>> 7, 61 | t);
|
|
2917
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
function normalize_seed(seed) {
|
|
2921
|
+
if (typeof seed === "number" && Number.isFinite(seed)) {
|
|
2922
|
+
return Math.trunc(seed) >>> 0;
|
|
2923
|
+
}
|
|
2924
|
+
return Math.floor(Math.random() * 4294967296) >>> 0;
|
|
2925
|
+
}
|
|
2926
|
+
function derive_iteration_seed(seed, iterationIndex) {
|
|
2927
|
+
return (seed ^ Math.imul(iterationIndex + 1, 2654435761)) >>> 0;
|
|
2928
|
+
}
|
|
2929
|
+
function run_monte_carlo_chunk(delivery, graph, options) {
|
|
2930
|
+
const scopedTasks = resolve_scoped_tasks(delivery, graph);
|
|
2931
|
+
if (scopedTasks.length === 0) {
|
|
2932
|
+
return Array.from({ length: options.iterations }, () => 0);
|
|
2933
|
+
}
|
|
2934
|
+
const includedTaskIds = new Set(scopedTasks.map((task) => task.id));
|
|
2935
|
+
const completionOffsets = [];
|
|
2936
|
+
const startIndex = options.start_index ?? 0;
|
|
2937
|
+
const seed = normalize_seed(options.seed);
|
|
2938
|
+
for (let i = 0; i < options.iterations; i += 1) {
|
|
2939
|
+
const rng = options.rng ?? create_seeded_rng(derive_iteration_seed(seed, startIndex + i));
|
|
2940
|
+
const sampledTasks = sample_ordered_tasks(scopedTasks, rng);
|
|
2941
|
+
const simulated = simulate_ordered_schedule(sampledTasks, includedTaskIds, graph, delivery, options.today);
|
|
2942
|
+
if (simulated.error) {
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
const offset = simulated.total_weeks > 0 ? (simulated.total_weeks - 1) * 7 + 6 : 0;
|
|
2946
|
+
completionOffsets.push(offset);
|
|
2947
|
+
}
|
|
2948
|
+
if (completionOffsets.length === 0 && scopedTasks.length > 0) {
|
|
2949
|
+
return [];
|
|
2950
|
+
}
|
|
2951
|
+
return completionOffsets;
|
|
2952
|
+
}
|
|
2953
|
+
function buildHistogram(offsets, origin, total) {
|
|
2954
|
+
if (offsets.length === 0 || total <= 0) return [];
|
|
2955
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
2956
|
+
for (const offset of offsets) {
|
|
2957
|
+
const week = Math.max(0, Math.floor(offset / 7));
|
|
2958
|
+
buckets.set(week, (buckets.get(week) ?? 0) + 1);
|
|
2959
|
+
}
|
|
2960
|
+
return Array.from(buckets.entries()).sort((a, b) => a[0] - b[0]).map(([week, count]) => ({
|
|
2961
|
+
week_index: week,
|
|
2962
|
+
start_date: isoDate2(addWeeks(origin, week)),
|
|
2963
|
+
end_date: isoDate2(addDays2(addWeeks(origin, week + 1), -1)),
|
|
2964
|
+
count,
|
|
2965
|
+
probability: count / total
|
|
2966
|
+
}));
|
|
2967
|
+
}
|
|
2968
|
+
function build_monte_carlo_result(offsets, origin, iterations, targetOffset) {
|
|
2969
|
+
const sorted = [...offsets].sort((a, b) => a - b);
|
|
2970
|
+
const p50Value = percentile(sorted, 0.5);
|
|
2971
|
+
const p75Value = percentile(sorted, 0.75);
|
|
2972
|
+
const p85Value = percentile(sorted, 0.85);
|
|
2973
|
+
const p95Value = percentile(sorted, 0.95);
|
|
2974
|
+
const avg = mean(sorted);
|
|
2975
|
+
const sigma = stddev(sorted, avg);
|
|
2976
|
+
let probabilityByTarget = null;
|
|
2977
|
+
if (targetOffset != null && sorted.length > 0) {
|
|
2978
|
+
let onTime = 0;
|
|
2979
|
+
for (const offset of sorted) {
|
|
2980
|
+
if (offset <= targetOffset) {
|
|
2981
|
+
onTime += 1;
|
|
2982
|
+
} else {
|
|
2983
|
+
break;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
probabilityByTarget = onTime / sorted.length;
|
|
2987
|
+
}
|
|
2988
|
+
return {
|
|
2989
|
+
iterations,
|
|
2990
|
+
successful_iterations: sorted.length,
|
|
2991
|
+
p50: Number.isFinite(p50Value) ? fromDayOffset(origin, p50Value) : null,
|
|
2992
|
+
p75: Number.isFinite(p75Value) ? fromDayOffset(origin, p75Value) : null,
|
|
2993
|
+
p85: Number.isFinite(p85Value) ? fromDayOffset(origin, p85Value) : null,
|
|
2994
|
+
p95: Number.isFinite(p95Value) ? fromDayOffset(origin, p95Value) : null,
|
|
2995
|
+
mean_date: Number.isFinite(avg) ? fromDayOffset(origin, Math.round(avg)) : null,
|
|
2996
|
+
stddev_days: Number.isFinite(sigma) ? sigma : 0,
|
|
2997
|
+
probability_by_target: probabilityByTarget,
|
|
2998
|
+
histogram: buildHistogram(sorted, origin, sorted.length)
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
function default_worker_count(workers) {
|
|
3002
|
+
if (typeof workers === "number" && Number.isInteger(workers) && workers > 0) {
|
|
3003
|
+
return workers;
|
|
3004
|
+
}
|
|
3005
|
+
return DEFAULT_MONTE_CARLO_WORKERS;
|
|
3006
|
+
}
|
|
3007
|
+
function current_module_url() {
|
|
3008
|
+
try {
|
|
3009
|
+
return (0, eval)("import.meta.url");
|
|
3010
|
+
} catch {
|
|
3011
|
+
if (typeof __filename === "string") {
|
|
3012
|
+
return (0, import_node_url.pathToFileURL)(__filename).href;
|
|
3013
|
+
}
|
|
3014
|
+
return null;
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
function resolve_worker_url() {
|
|
3018
|
+
const moduleUrl = current_module_url();
|
|
3019
|
+
if (!moduleUrl) {
|
|
3020
|
+
return null;
|
|
3021
|
+
}
|
|
3022
|
+
if (moduleUrl.endsWith(".ts")) {
|
|
3023
|
+
return null;
|
|
3024
|
+
}
|
|
3025
|
+
const candidates = [
|
|
3026
|
+
new URL("./planning/monte-carlo-worker.js", moduleUrl),
|
|
3027
|
+
new URL("./planning/monte-carlo-worker.cjs", moduleUrl),
|
|
3028
|
+
new URL("./monte-carlo-worker.js", moduleUrl),
|
|
3029
|
+
new URL("./monte-carlo-worker.cjs", moduleUrl)
|
|
3030
|
+
];
|
|
3031
|
+
for (const candidate of candidates) {
|
|
3032
|
+
if (import_node_fs10.default.existsSync((0, import_node_url.fileURLToPath)(candidate))) {
|
|
3033
|
+
return candidate;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return null;
|
|
3037
|
+
}
|
|
3038
|
+
function resolve_worker_exec_argv(workerUrl) {
|
|
3039
|
+
void workerUrl;
|
|
3040
|
+
return void 0;
|
|
3041
|
+
}
|
|
3042
|
+
function should_use_worker_threads(iterations, options, workerUrl) {
|
|
3043
|
+
return Boolean(workerUrl) && !options.rng && iterations > 1e3 && default_worker_count(options.workers) > 1;
|
|
3044
|
+
}
|
|
3045
|
+
async function run_monte_carlo_in_workers(delivery, graph, options) {
|
|
3046
|
+
const workerUrl = resolve_worker_url();
|
|
3047
|
+
if (!workerUrl) {
|
|
3048
|
+
return run_monte_carlo_chunk(delivery, graph, options);
|
|
3049
|
+
}
|
|
3050
|
+
const desiredWorkers = Math.min(default_worker_count(options.workers), options.iterations, import_node_os.default.availableParallelism());
|
|
3051
|
+
if (desiredWorkers <= 1) {
|
|
3052
|
+
return run_monte_carlo_chunk(delivery, graph, options);
|
|
3053
|
+
}
|
|
3054
|
+
const execArgv = resolve_worker_exec_argv(workerUrl);
|
|
3055
|
+
const baseChunk = Math.floor(options.iterations / desiredWorkers);
|
|
3056
|
+
const remainder = options.iterations % desiredWorkers;
|
|
3057
|
+
let startIndex = 0;
|
|
3058
|
+
const jobs = [];
|
|
3059
|
+
for (let index = 0; index < desiredWorkers; index += 1) {
|
|
3060
|
+
const chunkIterations = baseChunk + (index < remainder ? 1 : 0);
|
|
3061
|
+
if (chunkIterations <= 0) continue;
|
|
3062
|
+
const payload = {
|
|
3063
|
+
delivery,
|
|
3064
|
+
graph,
|
|
3065
|
+
today: typeof options.today === "string" ? options.today : isoDate2(asDate(options.today)),
|
|
3066
|
+
iterations: chunkIterations,
|
|
3067
|
+
start_index: startIndex,
|
|
3068
|
+
seed: options.seed
|
|
3069
|
+
};
|
|
3070
|
+
startIndex += chunkIterations;
|
|
3071
|
+
jobs.push(
|
|
3072
|
+
new Promise((resolve, reject) => {
|
|
3073
|
+
const worker = new import_node_worker_threads.Worker(workerUrl, {
|
|
3074
|
+
workerData: payload,
|
|
3075
|
+
execArgv
|
|
3076
|
+
});
|
|
3077
|
+
worker.once("message", (message) => {
|
|
3078
|
+
resolve(message);
|
|
3079
|
+
});
|
|
3080
|
+
worker.once("error", reject);
|
|
3081
|
+
worker.once("exit", (code) => {
|
|
3082
|
+
if (code !== 0) {
|
|
3083
|
+
reject(new Error(`Monte Carlo worker exited with code ${code}.`));
|
|
3084
|
+
}
|
|
3085
|
+
});
|
|
3086
|
+
})
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
const chunks = await Promise.all(jobs);
|
|
3090
|
+
return chunks.flat();
|
|
3091
|
+
}
|
|
3092
|
+
async function monte_carlo_forecast(delivery, graph, options = {}) {
|
|
3093
|
+
const iterations = typeof options.iterations === "number" && Number.isInteger(options.iterations) && options.iterations > 0 ? options.iterations : 1e4;
|
|
3094
|
+
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
3095
|
+
const origin = startOfWeek2(asDate(today));
|
|
3096
|
+
const seed = normalize_seed(options.seed);
|
|
3097
|
+
const targetOffset = delivery.target_date ? toDayOffset(origin, delivery.target_date) : null;
|
|
3098
|
+
const workerUrl = resolve_worker_url();
|
|
3099
|
+
const completionOffsets = should_use_worker_threads(iterations, options, workerUrl) ? await run_monte_carlo_in_workers(delivery, graph, {
|
|
3100
|
+
iterations,
|
|
3101
|
+
today,
|
|
3102
|
+
seed,
|
|
3103
|
+
workers: options.workers
|
|
3104
|
+
}) : run_monte_carlo_chunk(delivery, graph, {
|
|
3105
|
+
iterations,
|
|
3106
|
+
today,
|
|
3107
|
+
seed,
|
|
3108
|
+
rng: options.rng
|
|
3109
|
+
});
|
|
3110
|
+
return build_monte_carlo_result(completionOffsets, origin, iterations, targetOffset);
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// src/planning/feasibility.ts
|
|
3114
|
+
var DEFAULT_ESTIMATION_CONFIG = {
|
|
3115
|
+
version: 2,
|
|
3116
|
+
project: {
|
|
3117
|
+
name: "COOP",
|
|
3118
|
+
id: "coop"
|
|
3119
|
+
},
|
|
3120
|
+
defaults: {
|
|
3121
|
+
task: {
|
|
3122
|
+
complexity: "medium"
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
};
|
|
3126
|
+
function asDate2(value) {
|
|
3127
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
3128
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
3129
|
+
if (Number.isNaN(parsed.valueOf())) {
|
|
3130
|
+
throw new Error(`Invalid date '${value}'.`);
|
|
3131
|
+
}
|
|
3132
|
+
return parsed;
|
|
3133
|
+
}
|
|
3134
|
+
function normalize_track2(track) {
|
|
3135
|
+
return (track ?? "unassigned").trim().toLowerCase();
|
|
3136
|
+
}
|
|
3137
|
+
function isBusinessDay2(date) {
|
|
3138
|
+
const day = date.getUTCDay();
|
|
3139
|
+
return day >= 1 && day <= 5;
|
|
3140
|
+
}
|
|
3141
|
+
function business_days(from, to) {
|
|
3142
|
+
const start = asDate2(from);
|
|
3143
|
+
const end = asDate2(to);
|
|
3144
|
+
if (start.getTime() === end.getTime()) return 0;
|
|
3145
|
+
const forward = start.getTime() < end.getTime();
|
|
3146
|
+
const low = forward ? start : end;
|
|
3147
|
+
const high = forward ? end : start;
|
|
3148
|
+
let count = 0;
|
|
3149
|
+
const cursor = new Date(low.getTime());
|
|
3150
|
+
while (cursor < high) {
|
|
3151
|
+
if (isBusinessDay2(cursor)) count += 1;
|
|
3152
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
3153
|
+
}
|
|
3154
|
+
return forward ? count : -count;
|
|
3155
|
+
}
|
|
3156
|
+
function sumTaskEffort(tasks) {
|
|
3157
|
+
let total = 0;
|
|
3158
|
+
for (const task of tasks) {
|
|
3159
|
+
total += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG);
|
|
3160
|
+
}
|
|
3161
|
+
return total;
|
|
3162
|
+
}
|
|
3163
|
+
function sumPert(tasks) {
|
|
3164
|
+
let mean3 = 0;
|
|
3165
|
+
let variance = 0;
|
|
3166
|
+
for (const task of tasks) {
|
|
3167
|
+
if (task.estimate) {
|
|
3168
|
+
mean3 += pert_hours(task.estimate);
|
|
3169
|
+
const sigma = pert_stddev(task.estimate);
|
|
3170
|
+
variance += sigma * sigma;
|
|
3171
|
+
} else {
|
|
3172
|
+
mean3 += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
return {
|
|
3176
|
+
mean: mean3,
|
|
3177
|
+
stddev: Math.sqrt(variance)
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
function find_track_slots_hours(slots, track) {
|
|
3181
|
+
const direct = slots.get(track);
|
|
3182
|
+
if (direct) {
|
|
3183
|
+
let sum2 = 0;
|
|
3184
|
+
for (const value of direct.values()) sum2 += value;
|
|
3185
|
+
return sum2;
|
|
3186
|
+
}
|
|
3187
|
+
const fallback = slots.get("unassigned");
|
|
3188
|
+
if (!fallback) return 0;
|
|
3189
|
+
let sum = 0;
|
|
3190
|
+
for (const value of fallback.values()) sum += value;
|
|
3191
|
+
return sum;
|
|
3192
|
+
}
|
|
3193
|
+
function track_effort(tasks) {
|
|
3194
|
+
const out = /* @__PURE__ */ new Map();
|
|
3195
|
+
for (const task of tasks) {
|
|
3196
|
+
const track = normalize_track2(task.track);
|
|
3197
|
+
out.set(track, (out.get(track) ?? 0) + effort_or_default(task, DEFAULT_ESTIMATION_CONFIG));
|
|
3198
|
+
}
|
|
3199
|
+
return out;
|
|
3200
|
+
}
|
|
3201
|
+
function analyze_feasibility(deliveryId, graph, today) {
|
|
3202
|
+
const delivery = graph.deliveries.get(deliveryId);
|
|
3203
|
+
if (!delivery) {
|
|
3204
|
+
throw new Error(`Delivery '${deliveryId}' not found.`);
|
|
3205
|
+
}
|
|
3206
|
+
const include = new Set(delivery.scope.include);
|
|
3207
|
+
for (const excluded of delivery.scope.exclude) {
|
|
3208
|
+
include.delete(excluded);
|
|
3209
|
+
}
|
|
3210
|
+
const scope_tasks = Array.from(include).map((id) => graph.nodes.get(id)).filter((task) => Boolean(task));
|
|
3211
|
+
const subgraph = extract_subgraph(
|
|
3212
|
+
graph,
|
|
3213
|
+
scope_tasks.map((task) => task.id)
|
|
3214
|
+
);
|
|
3215
|
+
const external_deps = find_external_dependencies(subgraph, graph);
|
|
3216
|
+
const done_tasks = scope_tasks.filter((task) => task.status === "done");
|
|
3217
|
+
const remaining_tasks = scope_tasks.filter(
|
|
3218
|
+
(task) => task.status !== "done" && task.status !== "canceled"
|
|
3219
|
+
);
|
|
3220
|
+
const total_effort = sumTaskEffort(remaining_tasks);
|
|
3221
|
+
const pert = sumPert(remaining_tasks);
|
|
3222
|
+
const cpm = compute_critical_path(delivery, graph);
|
|
3223
|
+
const ledger = build_capacity_ledger(delivery, graph.resources, today);
|
|
3224
|
+
const simulation = simulate_schedule(remaining_tasks, graph, delivery, today);
|
|
3225
|
+
const tracks = new Set(remaining_tasks.map((task) => normalize_track2(task.track)));
|
|
3226
|
+
const capacity_by_track = {};
|
|
3227
|
+
for (const track of tracks) {
|
|
3228
|
+
capacity_by_track[track] = find_track_slots_hours(ledger.slots, track);
|
|
3229
|
+
}
|
|
3230
|
+
const total_capacity = Object.values(capacity_by_track).reduce((sum, value) => sum + value, 0);
|
|
3231
|
+
const budget_hours = delivery.budget.engineering_hours ?? Number.POSITIVE_INFINITY;
|
|
3232
|
+
const budget_cost = delivery.budget.cost_usd ?? Number.POSITIVE_INFINITY;
|
|
3233
|
+
const total_cost = remaining_tasks.reduce((sum, task) => sum + (task.resources?.cost_usd ?? 0), 0);
|
|
3234
|
+
let feasible = true;
|
|
3235
|
+
const risks = [];
|
|
3236
|
+
if (total_effort > budget_hours) {
|
|
3237
|
+
feasible = false;
|
|
3238
|
+
risks.push({
|
|
3239
|
+
type: "budget_exceeded",
|
|
3240
|
+
message: `Required ${total_effort.toFixed(1)}h exceeds budget ${budget_hours.toFixed(1)}h.`,
|
|
3241
|
+
overage_hours: total_effort - budget_hours
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
if (total_cost > budget_cost) {
|
|
3245
|
+
feasible = false;
|
|
3246
|
+
risks.push({
|
|
3247
|
+
type: "cost_budget_exceeded",
|
|
3248
|
+
message: `Required $${total_cost.toFixed(2)} exceeds budget $${budget_cost.toFixed(2)}.`,
|
|
3249
|
+
overage_cost_usd: total_cost - budget_cost
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
if (total_effort > total_capacity) {
|
|
3253
|
+
feasible = false;
|
|
3254
|
+
risks.push({
|
|
3255
|
+
type: "capacity_exceeded",
|
|
3256
|
+
message: `Required ${total_effort.toFixed(1)}h exceeds capacity ${total_capacity.toFixed(1)}h.`,
|
|
3257
|
+
overage_hours: total_effort - total_capacity
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
if (simulation.error) {
|
|
3261
|
+
feasible = false;
|
|
3262
|
+
risks.push({
|
|
3263
|
+
type: "schedule_overflow",
|
|
3264
|
+
message: `Simulation overflow while scheduling task ${simulation.error.task}.`,
|
|
3265
|
+
tasks: [simulation.error.task]
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
if (delivery.target_date && simulation.projected_completion && simulation.projected_completion > delivery.target_date) {
|
|
3269
|
+
feasible = false;
|
|
3270
|
+
risks.push({
|
|
3271
|
+
type: "schedule_exceeded",
|
|
3272
|
+
message: `Projected ${simulation.projected_completion} exceeds target ${delivery.target_date}.`,
|
|
3273
|
+
overage_days: business_days(delivery.target_date, simulation.projected_completion)
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
const effort_by_track = track_effort(remaining_tasks);
|
|
3277
|
+
for (const [track, effort] of effort_by_track.entries()) {
|
|
3278
|
+
const capacity = capacity_by_track[track] ?? 0;
|
|
3279
|
+
if (capacity <= 0) continue;
|
|
3280
|
+
const utilization = effort / capacity;
|
|
3281
|
+
if (utilization > 0.85) {
|
|
3282
|
+
risks.push({
|
|
3283
|
+
type: "high_utilization",
|
|
3284
|
+
message: `Track '${track}' at ${Math.round(utilization * 100)}% utilization.`,
|
|
3285
|
+
track,
|
|
3286
|
+
utilization
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
if (external_deps.length > 0) {
|
|
3291
|
+
risks.push({
|
|
3292
|
+
type: "external_dependencies",
|
|
3293
|
+
message: `${external_deps.length} tasks depend on work outside this delivery.`,
|
|
3294
|
+
tasks: external_deps
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
const buffer_days = delivery.target_date && simulation.projected_completion ? business_days(simulation.projected_completion, delivery.target_date) : null;
|
|
3298
|
+
return {
|
|
3299
|
+
delivery: deliveryId,
|
|
3300
|
+
status: feasible ? "FEASIBLE" : "NOT_FEASIBLE",
|
|
3301
|
+
risks,
|
|
3302
|
+
summary: {
|
|
3303
|
+
total_tasks: scope_tasks.length,
|
|
3304
|
+
completed: done_tasks.length,
|
|
3305
|
+
remaining: remaining_tasks.length,
|
|
3306
|
+
effort_hours: {
|
|
3307
|
+
required: total_effort,
|
|
3308
|
+
budget: budget_hours
|
|
3309
|
+
},
|
|
3310
|
+
capacity_hours: {
|
|
3311
|
+
available: total_capacity,
|
|
3312
|
+
by_track: capacity_by_track
|
|
3313
|
+
},
|
|
3314
|
+
pert,
|
|
3315
|
+
dates: {
|
|
3316
|
+
target: delivery.target_date,
|
|
3317
|
+
projected: simulation.projected_completion,
|
|
3318
|
+
buffer_days
|
|
3319
|
+
},
|
|
3320
|
+
critical_path: cpm.critical_path,
|
|
3321
|
+
critical_path_hours: cpm.project_duration_hours
|
|
3322
|
+
},
|
|
3323
|
+
simulation
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
// src/planning/risk-detector.ts
|
|
3328
|
+
function toIsoTimestamp(now) {
|
|
3329
|
+
if (typeof now === "string") {
|
|
3330
|
+
if (now.includes("T")) return now;
|
|
3331
|
+
return `${now}T00:00:00.000Z`;
|
|
3332
|
+
}
|
|
3333
|
+
const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
|
|
3334
|
+
return date.toISOString();
|
|
3335
|
+
}
|
|
3336
|
+
function detect_delivery_risks(delivery, graph, velocity, options = {}) {
|
|
3337
|
+
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
3338
|
+
const feasibility = analyze_feasibility(delivery.id, graph, today);
|
|
3339
|
+
const criticalPath = compute_critical_path(delivery, graph);
|
|
3340
|
+
const risks = [];
|
|
3341
|
+
if (velocity.trend === "decelerating" && (feasibility.status === "NOT_FEASIBLE" || feasibility.summary.dates.target && feasibility.summary.dates.projected && feasibility.summary.dates.projected > feasibility.summary.dates.target || typeof feasibility.summary.dates.buffer_days === "number" && feasibility.summary.dates.buffer_days < 0)) {
|
|
3342
|
+
risks.push({
|
|
3343
|
+
type: "behind_schedule",
|
|
3344
|
+
message: `Delivery '${delivery.id}' is slipping while velocity is decelerating.`
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
for (const utilization of feasibility.simulation.utilization_by_track) {
|
|
3348
|
+
if (utilization.utilization >= 0.9) {
|
|
3349
|
+
risks.push({
|
|
3350
|
+
type: "capacity_crunch",
|
|
3351
|
+
message: `Track '${utilization.track}' is at ${Math.round(utilization.utilization * 100)}% utilization.`,
|
|
3352
|
+
track: utilization.track
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
const notStarted = criticalPath.critical_path.filter((taskId) => {
|
|
3357
|
+
const task = graph.nodes.get(taskId);
|
|
3358
|
+
return task?.status === "todo" || task?.status === "blocked";
|
|
3359
|
+
});
|
|
3360
|
+
if (notStarted.length > 0) {
|
|
3361
|
+
risks.push({
|
|
3362
|
+
type: "critical_path_not_started",
|
|
3363
|
+
message: `${notStarted.length} critical path tasks are not started.`,
|
|
3364
|
+
task_ids: notStarted
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
const highRiskCritical = criticalPath.critical_path.filter((taskId) => {
|
|
3368
|
+
const task = graph.nodes.get(taskId);
|
|
3369
|
+
return task?.risk?.level === "high" || task?.risk?.level === "critical";
|
|
3370
|
+
});
|
|
3371
|
+
if (highRiskCritical.length > 0) {
|
|
3372
|
+
risks.push({
|
|
3373
|
+
type: "high_risk_critical_path",
|
|
3374
|
+
message: `${highRiskCritical.length} high-risk tasks are on the critical path.`,
|
|
3375
|
+
task_ids: highRiskCritical
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
if (risks.length > 0) {
|
|
3379
|
+
options.eventEmitter?.emit({
|
|
3380
|
+
type: "delivery.at_risk",
|
|
3381
|
+
timestamp: toIsoTimestamp(today),
|
|
3382
|
+
payload: {
|
|
3383
|
+
delivery_id: delivery.id,
|
|
3384
|
+
status: feasibility.status,
|
|
3385
|
+
risks: risks.map((risk) => risk.message)
|
|
3386
|
+
}
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
return risks;
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
// src/planning/scorer.ts
|
|
3393
|
+
var DEFAULT_SCORE_WEIGHTS = {
|
|
3394
|
+
priority: {
|
|
3395
|
+
p0: 100,
|
|
3396
|
+
p1: 75,
|
|
3397
|
+
p2: 50,
|
|
3398
|
+
p3: 25
|
|
3399
|
+
},
|
|
3400
|
+
urgency: {
|
|
3401
|
+
days_7: 40,
|
|
3402
|
+
days_14: 25,
|
|
3403
|
+
days_28: 10
|
|
3404
|
+
},
|
|
3405
|
+
dependency_unlock: {
|
|
3406
|
+
gte_5: 30,
|
|
3407
|
+
gte_3: 20,
|
|
3408
|
+
gte_1: 10,
|
|
3409
|
+
transitive_cap: 15
|
|
3410
|
+
},
|
|
3411
|
+
critical_path: 35,
|
|
3412
|
+
determinism: {
|
|
3413
|
+
high: 30,
|
|
3414
|
+
medium: 15,
|
|
3415
|
+
low: 0,
|
|
3416
|
+
experimental: -10
|
|
3417
|
+
},
|
|
3418
|
+
executor_fit: {
|
|
3419
|
+
match: 15,
|
|
3420
|
+
mismatch: -15,
|
|
3421
|
+
ci_match: 10
|
|
3422
|
+
},
|
|
3423
|
+
type: {
|
|
3424
|
+
bug: 10,
|
|
3425
|
+
spike: 5,
|
|
3426
|
+
feature: 0,
|
|
3427
|
+
chore: 0,
|
|
3428
|
+
epic: 0
|
|
3429
|
+
},
|
|
3430
|
+
complexity_penalty: {
|
|
3431
|
+
trivial: 0,
|
|
3432
|
+
small: 0,
|
|
3433
|
+
medium: -10,
|
|
3434
|
+
large: -25,
|
|
3435
|
+
unknown: -40
|
|
3436
|
+
},
|
|
3437
|
+
risk_penalty: {
|
|
3438
|
+
low: 0,
|
|
3439
|
+
medium: -10,
|
|
3440
|
+
high: -25,
|
|
3441
|
+
critical: -40
|
|
3442
|
+
}
|
|
3443
|
+
};
|
|
3444
|
+
function asDate3(value) {
|
|
3445
|
+
if (value instanceof Date) {
|
|
3446
|
+
return new Date(value.getTime());
|
|
3447
|
+
}
|
|
3448
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
3449
|
+
if (Number.isNaN(parsed.valueOf())) {
|
|
3450
|
+
throw new Error(`Invalid date '${value}'.`);
|
|
3451
|
+
}
|
|
3452
|
+
return parsed;
|
|
3453
|
+
}
|
|
3454
|
+
function calendar_days(today, target) {
|
|
3455
|
+
const from = asDate3(today);
|
|
3456
|
+
const to = asDate3(target);
|
|
3457
|
+
const millis = to.getTime() - from.getTime();
|
|
3458
|
+
return Math.floor(millis / (1e3 * 60 * 60 * 24));
|
|
3459
|
+
}
|
|
3460
|
+
function toMap(deliveries) {
|
|
3461
|
+
if (!deliveries) return /* @__PURE__ */ new Map();
|
|
3462
|
+
if (deliveries instanceof Map) return deliveries;
|
|
3463
|
+
return new Map(deliveries.map((delivery) => [delivery.id, delivery]));
|
|
3464
|
+
}
|
|
3465
|
+
function isRecord(value) {
|
|
3466
|
+
return typeof value === "object" && value !== null;
|
|
3467
|
+
}
|
|
3468
|
+
function number_or_default(value, fallback) {
|
|
3469
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
3470
|
+
}
|
|
3471
|
+
function configured_weights(config) {
|
|
3472
|
+
const rawWeights = config?.scheduling?.weights;
|
|
3473
|
+
if (!isRecord(rawWeights)) {
|
|
3474
|
+
return DEFAULT_SCORE_WEIGHTS;
|
|
3475
|
+
}
|
|
3476
|
+
const priority = isRecord(rawWeights.priority) ? rawWeights.priority : {};
|
|
3477
|
+
const urgency = isRecord(rawWeights.urgency) ? rawWeights.urgency : {};
|
|
3478
|
+
const dependency_unlock = isRecord(rawWeights.dependency_unlock) ? rawWeights.dependency_unlock : {};
|
|
3479
|
+
const determinism = isRecord(rawWeights.determinism) ? rawWeights.determinism : {};
|
|
3480
|
+
const executor_fit = isRecord(rawWeights.executor_fit) ? rawWeights.executor_fit : {};
|
|
3481
|
+
const type = isRecord(rawWeights.type) ? rawWeights.type : {};
|
|
3482
|
+
const complexity_penalty2 = isRecord(rawWeights.complexity_penalty) ? rawWeights.complexity_penalty : {};
|
|
3483
|
+
const risk_penalty2 = isRecord(rawWeights.risk_penalty) ? rawWeights.risk_penalty : {};
|
|
3484
|
+
return {
|
|
3485
|
+
priority: {
|
|
3486
|
+
p0: number_or_default(priority.p0, DEFAULT_SCORE_WEIGHTS.priority.p0),
|
|
3487
|
+
p1: number_or_default(priority.p1, DEFAULT_SCORE_WEIGHTS.priority.p1),
|
|
3488
|
+
p2: number_or_default(priority.p2, DEFAULT_SCORE_WEIGHTS.priority.p2),
|
|
3489
|
+
p3: number_or_default(priority.p3, DEFAULT_SCORE_WEIGHTS.priority.p3)
|
|
3490
|
+
},
|
|
3491
|
+
urgency: {
|
|
3492
|
+
days_7: number_or_default(urgency.days_7, DEFAULT_SCORE_WEIGHTS.urgency.days_7),
|
|
3493
|
+
days_14: number_or_default(urgency.days_14, DEFAULT_SCORE_WEIGHTS.urgency.days_14),
|
|
3494
|
+
days_28: number_or_default(urgency.days_28, DEFAULT_SCORE_WEIGHTS.urgency.days_28)
|
|
3495
|
+
},
|
|
3496
|
+
dependency_unlock: {
|
|
3497
|
+
gte_5: number_or_default(dependency_unlock.gte_5, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_5),
|
|
3498
|
+
gte_3: number_or_default(dependency_unlock.gte_3, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_3),
|
|
3499
|
+
gte_1: number_or_default(dependency_unlock.gte_1, DEFAULT_SCORE_WEIGHTS.dependency_unlock.gte_1),
|
|
3500
|
+
transitive_cap: number_or_default(
|
|
3501
|
+
dependency_unlock.transitive_cap,
|
|
3502
|
+
DEFAULT_SCORE_WEIGHTS.dependency_unlock.transitive_cap
|
|
3503
|
+
)
|
|
3504
|
+
},
|
|
3505
|
+
critical_path: number_or_default(rawWeights.critical_path, DEFAULT_SCORE_WEIGHTS.critical_path),
|
|
3506
|
+
determinism: {
|
|
3507
|
+
high: number_or_default(determinism.high, DEFAULT_SCORE_WEIGHTS.determinism.high),
|
|
3508
|
+
medium: number_or_default(determinism.medium, DEFAULT_SCORE_WEIGHTS.determinism.medium),
|
|
3509
|
+
low: number_or_default(determinism.low, DEFAULT_SCORE_WEIGHTS.determinism.low),
|
|
3510
|
+
experimental: number_or_default(determinism.experimental, DEFAULT_SCORE_WEIGHTS.determinism.experimental)
|
|
3511
|
+
},
|
|
3512
|
+
executor_fit: {
|
|
3513
|
+
match: number_or_default(executor_fit.match, DEFAULT_SCORE_WEIGHTS.executor_fit.match),
|
|
3514
|
+
mismatch: number_or_default(executor_fit.mismatch, DEFAULT_SCORE_WEIGHTS.executor_fit.mismatch),
|
|
3515
|
+
ci_match: number_or_default(executor_fit.ci_match, DEFAULT_SCORE_WEIGHTS.executor_fit.ci_match)
|
|
3516
|
+
},
|
|
3517
|
+
type: {
|
|
3518
|
+
bug: number_or_default(type.bug, DEFAULT_SCORE_WEIGHTS.type.bug),
|
|
3519
|
+
spike: number_or_default(type.spike, DEFAULT_SCORE_WEIGHTS.type.spike),
|
|
3520
|
+
feature: number_or_default(type.feature, DEFAULT_SCORE_WEIGHTS.type.feature),
|
|
3521
|
+
chore: number_or_default(type.chore, DEFAULT_SCORE_WEIGHTS.type.chore),
|
|
3522
|
+
epic: number_or_default(type.epic, DEFAULT_SCORE_WEIGHTS.type.epic)
|
|
3523
|
+
},
|
|
3524
|
+
complexity_penalty: {
|
|
3525
|
+
trivial: number_or_default(complexity_penalty2.trivial, DEFAULT_SCORE_WEIGHTS.complexity_penalty.trivial),
|
|
3526
|
+
small: number_or_default(complexity_penalty2.small, DEFAULT_SCORE_WEIGHTS.complexity_penalty.small),
|
|
3527
|
+
medium: number_or_default(complexity_penalty2.medium, DEFAULT_SCORE_WEIGHTS.complexity_penalty.medium),
|
|
3528
|
+
large: number_or_default(complexity_penalty2.large, DEFAULT_SCORE_WEIGHTS.complexity_penalty.large),
|
|
3529
|
+
unknown: number_or_default(complexity_penalty2.unknown, DEFAULT_SCORE_WEIGHTS.complexity_penalty.unknown)
|
|
3530
|
+
},
|
|
3531
|
+
risk_penalty: {
|
|
3532
|
+
low: number_or_default(risk_penalty2.low, DEFAULT_SCORE_WEIGHTS.risk_penalty.low),
|
|
3533
|
+
medium: number_or_default(risk_penalty2.medium, DEFAULT_SCORE_WEIGHTS.risk_penalty.medium),
|
|
3534
|
+
high: number_or_default(risk_penalty2.high, DEFAULT_SCORE_WEIGHTS.risk_penalty.high),
|
|
3535
|
+
critical: number_or_default(risk_penalty2.critical, DEFAULT_SCORE_WEIGHTS.risk_penalty.critical)
|
|
3536
|
+
}
|
|
3537
|
+
};
|
|
3538
|
+
}
|
|
3539
|
+
function unresolved_dependencies(task, graph) {
|
|
3540
|
+
return (task.depends_on ?? []).filter((depId) => {
|
|
3541
|
+
const dep = graph.nodes.get(depId);
|
|
3542
|
+
return !dep || dep.status !== "done" && dep.status !== "canceled";
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
function priority_weight(task, config) {
|
|
3546
|
+
const weights = configured_weights(config);
|
|
3547
|
+
const priority = task.priority ?? "p2";
|
|
3548
|
+
return weights.priority[priority];
|
|
3549
|
+
}
|
|
3550
|
+
function urgency_weight(task, deliveries, today, config) {
|
|
3551
|
+
if (!task.delivery) return 0;
|
|
3552
|
+
const delivery = toMap(deliveries).get(task.delivery);
|
|
3553
|
+
if (!delivery?.target_date) return 0;
|
|
3554
|
+
const weights = configured_weights(config);
|
|
3555
|
+
const daysRemaining = calendar_days(today, delivery.target_date);
|
|
3556
|
+
if (daysRemaining < 7) return weights.urgency.days_7;
|
|
3557
|
+
if (daysRemaining < 14) return weights.urgency.days_14;
|
|
3558
|
+
if (daysRemaining < 28) return weights.urgency.days_28;
|
|
3559
|
+
return 0;
|
|
3560
|
+
}
|
|
3561
|
+
function dependency_unlock_weight(taskId, graph, config) {
|
|
3562
|
+
const weights = configured_weights(config);
|
|
3563
|
+
const reverse = graph.reverse.get(taskId) ?? /* @__PURE__ */ new Set();
|
|
3564
|
+
const directBlocked = [];
|
|
3565
|
+
for (const dependentId of reverse) {
|
|
3566
|
+
const dependent = graph.nodes.get(dependentId);
|
|
3567
|
+
if (!dependent) continue;
|
|
3568
|
+
if (compute_readiness(dependent, graph) !== "blocked") continue;
|
|
3569
|
+
const unresolved = unresolved_dependencies(dependent, graph);
|
|
3570
|
+
if (unresolved.length === 1 && unresolved[0] === taskId) {
|
|
3571
|
+
directBlocked.push(dependentId);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
const directCount = directBlocked.length;
|
|
3575
|
+
let directWeight = 0;
|
|
3576
|
+
if (directCount >= 5) {
|
|
3577
|
+
directWeight = weights.dependency_unlock.gte_5;
|
|
3578
|
+
} else if (directCount >= 3) {
|
|
3579
|
+
directWeight = weights.dependency_unlock.gte_3;
|
|
3580
|
+
} else if (directCount >= 1) {
|
|
3581
|
+
directWeight = weights.dependency_unlock.gte_1;
|
|
3582
|
+
}
|
|
3583
|
+
const transitive = /* @__PURE__ */ new Set();
|
|
3584
|
+
for (const directId of directBlocked) {
|
|
3585
|
+
const reverseDepth2 = graph.reverse.get(directId) ?? /* @__PURE__ */ new Set();
|
|
3586
|
+
for (const transitiveId of reverseDepth2) {
|
|
3587
|
+
const task = graph.nodes.get(transitiveId);
|
|
3588
|
+
if (!task) continue;
|
|
3589
|
+
if (compute_readiness(task, graph) === "blocked") {
|
|
3590
|
+
transitive.add(transitiveId);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
const transitiveWeight = Math.min(transitive.size * 2, weights.dependency_unlock.transitive_cap);
|
|
3595
|
+
return directWeight + transitiveWeight;
|
|
3596
|
+
}
|
|
3597
|
+
function critical_path_weight(taskId, cpm, config) {
|
|
3598
|
+
if (!cpm) return 0;
|
|
3599
|
+
const weights = configured_weights(config);
|
|
3600
|
+
const isCritical = cpm.critical_path.includes(taskId);
|
|
3601
|
+
return isCritical ? weights.critical_path : 0;
|
|
3602
|
+
}
|
|
3603
|
+
function determinism_weight(task, config) {
|
|
3604
|
+
const weights = configured_weights(config);
|
|
3605
|
+
const defaultDeterminism = config?.defaults?.task?.determinism ?? "medium";
|
|
3606
|
+
const determinism = task.determinism ?? defaultDeterminism;
|
|
3607
|
+
return weights.determinism[determinism];
|
|
3608
|
+
}
|
|
3609
|
+
function executor_fit_weight(task, targetExecutor, config) {
|
|
3610
|
+
if (!targetExecutor) return 0;
|
|
3611
|
+
const weights = configured_weights(config);
|
|
3612
|
+
const defaultDeterminism = config?.defaults?.task?.determinism ?? "medium";
|
|
3613
|
+
const determinism = task.determinism ?? defaultDeterminism;
|
|
3614
|
+
if (targetExecutor === "ai") {
|
|
3615
|
+
return determinism === "high" || determinism === "medium" ? weights.executor_fit.match : weights.executor_fit.mismatch;
|
|
3616
|
+
}
|
|
3617
|
+
if (targetExecutor === "human") {
|
|
3618
|
+
return determinism === "low" || determinism === "experimental" ? weights.executor_fit.match : weights.executor_fit.mismatch;
|
|
3619
|
+
}
|
|
3620
|
+
if (targetExecutor === "ci") {
|
|
3621
|
+
const runbook = task.execution?.runbook ?? [];
|
|
3622
|
+
const ciReady = task.type === "chore" && runbook.length > 0 && runbook.every((step) => step.action === "run");
|
|
3623
|
+
return ciReady ? weights.executor_fit.ci_match : weights.executor_fit.mismatch;
|
|
3624
|
+
}
|
|
3625
|
+
return 0;
|
|
3626
|
+
}
|
|
3627
|
+
function type_weight(task, config) {
|
|
3628
|
+
const weights = configured_weights(config);
|
|
3629
|
+
return weights.type[task.type];
|
|
3630
|
+
}
|
|
3631
|
+
function complexity_penalty(task, config) {
|
|
3632
|
+
const weights = configured_weights(config);
|
|
3633
|
+
const complexity = task.complexity ?? config?.defaults?.task?.complexity ?? "medium";
|
|
3634
|
+
return weights.complexity_penalty[complexity];
|
|
3635
|
+
}
|
|
3636
|
+
function risk_penalty(task, config) {
|
|
3637
|
+
const weights = configured_weights(config);
|
|
3638
|
+
const level = task.risk?.level ?? "low";
|
|
3639
|
+
return weights.risk_penalty[level];
|
|
3640
|
+
}
|
|
3641
|
+
function compute_score(task, graph, context = {}) {
|
|
3642
|
+
const deliveries = context.deliveries ?? graph.deliveries;
|
|
3643
|
+
const today = context.today ?? /* @__PURE__ */ new Date();
|
|
3644
|
+
const config = context.config;
|
|
3645
|
+
return priority_weight(task, config) + urgency_weight(task, deliveries, today, config) + dependency_unlock_weight(task.id, graph, config) + critical_path_weight(task.id, context.cpm, config) + determinism_weight(task, config) + executor_fit_weight(task, context.target_executor, config) + type_weight(task, config) + complexity_penalty(task, config) + risk_penalty(task, config);
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
// src/planning/scheduler.ts
|
|
3649
|
+
function asDate4(value) {
|
|
3650
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
3651
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
3652
|
+
if (Number.isNaN(parsed.valueOf())) {
|
|
3653
|
+
throw new Error(`Invalid date '${value}'.`);
|
|
3654
|
+
}
|
|
3655
|
+
return parsed;
|
|
3656
|
+
}
|
|
3657
|
+
function isoDate3(value) {
|
|
3658
|
+
return value.toISOString().slice(0, 10);
|
|
3659
|
+
}
|
|
3660
|
+
function addDays3(value, days) {
|
|
3661
|
+
const out = new Date(value.getTime());
|
|
3662
|
+
out.setUTCDate(out.getUTCDate() + days);
|
|
3663
|
+
return out;
|
|
3664
|
+
}
|
|
3665
|
+
function clone_ledger2(ledger) {
|
|
3666
|
+
return {
|
|
3667
|
+
slots: new Map(
|
|
3668
|
+
Array.from(ledger.slots.entries(), ([track, byWeek]) => [track, new Map(byWeek)])
|
|
3669
|
+
),
|
|
3670
|
+
weeks: new Map(ledger.weeks),
|
|
3671
|
+
ai_tokens: new Map(ledger.ai_tokens),
|
|
3672
|
+
ai_tokens_by_agent: new Map(
|
|
3673
|
+
Array.from(ledger.ai_tokens_by_agent.entries(), ([agent, byDay]) => [agent, new Map(byDay)])
|
|
3674
|
+
),
|
|
3675
|
+
ai_tokens_consumed_by_agent: new Map(
|
|
3676
|
+
Array.from(ledger.ai_tokens_consumed_by_agent.entries(), ([agent, byDay]) => [
|
|
3677
|
+
agent,
|
|
3678
|
+
new Map(byDay)
|
|
3679
|
+
])
|
|
3680
|
+
)
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
function normalize_track3(track) {
|
|
3684
|
+
return (track ?? "unassigned").trim().toLowerCase();
|
|
3685
|
+
}
|
|
3686
|
+
function find_track_slots2(ledger, track) {
|
|
3687
|
+
const normalized = normalize_track3(track);
|
|
3688
|
+
return ledger.slots.get(normalized) ?? ledger.slots.get("unassigned") ?? null;
|
|
3689
|
+
}
|
|
3690
|
+
function check_capacity(task, ledger) {
|
|
3691
|
+
try {
|
|
3692
|
+
const slots = find_track_slots2(ledger, task.track);
|
|
3693
|
+
if (!slots) return false;
|
|
3694
|
+
const week0 = slots.get(0) ?? 0;
|
|
3695
|
+
if (week0 <= 0) return false;
|
|
3696
|
+
const attempt = clone_ledger2(ledger);
|
|
3697
|
+
return allocate(attempt, task, 0).success;
|
|
3698
|
+
} catch {
|
|
3699
|
+
return false;
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
function advisory_delivery(graph, tasks, options) {
|
|
3703
|
+
if (options.delivery) {
|
|
3704
|
+
const existing = graph.deliveries.get(options.delivery);
|
|
3705
|
+
if (existing) {
|
|
3706
|
+
return {
|
|
3707
|
+
id: existing.id,
|
|
3708
|
+
name: existing.name,
|
|
3709
|
+
status: "planning",
|
|
3710
|
+
target_date: existing.target_date ?? isoDate3(addDays3(asDate4(options.today ?? /* @__PURE__ */ new Date()), 56)),
|
|
3711
|
+
started_date: existing.started_date,
|
|
3712
|
+
delivered_date: existing.delivered_date,
|
|
3713
|
+
budget: existing.budget,
|
|
3714
|
+
capacity_profiles: existing.capacity_profiles,
|
|
3715
|
+
scope: existing.scope
|
|
3716
|
+
};
|
|
838
3717
|
}
|
|
839
|
-
ordered[key] = raw[key];
|
|
840
3718
|
}
|
|
841
|
-
|
|
3719
|
+
const targetDate = isoDate3(addDays3(asDate4(options.today ?? /* @__PURE__ */ new Date()), 56));
|
|
3720
|
+
return {
|
|
3721
|
+
id: "__SCHEDULE__",
|
|
3722
|
+
name: "Advisory Schedule",
|
|
3723
|
+
status: "planning",
|
|
3724
|
+
target_date: targetDate,
|
|
3725
|
+
started_date: null,
|
|
3726
|
+
delivered_date: null,
|
|
3727
|
+
budget: {},
|
|
3728
|
+
capacity_profiles: [],
|
|
3729
|
+
scope: {
|
|
3730
|
+
include: tasks.map((task) => task.id),
|
|
3731
|
+
exclude: []
|
|
3732
|
+
}
|
|
3733
|
+
};
|
|
842
3734
|
}
|
|
843
|
-
function
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
3735
|
+
function resolve_cpm(graph, options) {
|
|
3736
|
+
if (options.delivery) {
|
|
3737
|
+
const delivery = graph.deliveries.get(options.delivery);
|
|
3738
|
+
if (!delivery) return void 0;
|
|
3739
|
+
return compute_critical_path(delivery, graph);
|
|
3740
|
+
}
|
|
3741
|
+
const preferred = Array.from(graph.deliveries.values()).find(
|
|
3742
|
+
(delivery) => delivery.status === "committed" || delivery.status === "in_progress"
|
|
3743
|
+
);
|
|
3744
|
+
if (!preferred) return void 0;
|
|
3745
|
+
return compute_critical_path(preferred, graph);
|
|
3746
|
+
}
|
|
3747
|
+
function schedule_next(graph, options = {}) {
|
|
3748
|
+
const ready_tasks = Array.from(graph.nodes.values()).filter(
|
|
3749
|
+
(task) => compute_readiness(task, graph) === "ready"
|
|
3750
|
+
);
|
|
3751
|
+
let filtered = ready_tasks;
|
|
3752
|
+
if (options.track) {
|
|
3753
|
+
filtered = filtered.filter((task) => (task.track ?? "unassigned") === options.track);
|
|
3754
|
+
}
|
|
3755
|
+
const deliveryFilter = options.delivery;
|
|
3756
|
+
if (deliveryFilter) {
|
|
3757
|
+
const scope = graph.deliveries.get(deliveryFilter)?.scope.include ?? [];
|
|
3758
|
+
const scopeSet = new Set(scope);
|
|
3759
|
+
filtered = filtered.filter(
|
|
3760
|
+
(task) => task.delivery === deliveryFilter || scopeSet.has(task.id)
|
|
3761
|
+
);
|
|
3762
|
+
}
|
|
3763
|
+
if (options.executor) {
|
|
3764
|
+
filtered = filtered.filter(
|
|
3765
|
+
(task) => task.execution?.executor === options.executor || task.execution?.executor == null
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
const cpm = resolve_cpm(graph, options);
|
|
3769
|
+
const scored = filtered.map((task) => ({
|
|
3770
|
+
task,
|
|
3771
|
+
score: compute_score(task, graph, {
|
|
3772
|
+
deliveries: graph.deliveries,
|
|
3773
|
+
cpm,
|
|
3774
|
+
today: options.today ?? /* @__PURE__ */ new Date(),
|
|
3775
|
+
target_executor: options.executor,
|
|
3776
|
+
config: options.config
|
|
3777
|
+
}),
|
|
3778
|
+
readiness: compute_readiness(task, graph),
|
|
3779
|
+
fits_capacity: true,
|
|
3780
|
+
fits_wip: true
|
|
3781
|
+
}));
|
|
3782
|
+
scored.sort((a, b) => {
|
|
3783
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
3784
|
+
return a.task.id.localeCompare(b.task.id);
|
|
3785
|
+
});
|
|
3786
|
+
const limit = typeof options.limit === "number" && Number.isInteger(options.limit) && options.limit > 0 ? options.limit : void 0;
|
|
3787
|
+
const output = typeof limit === "number" ? scored.slice(0, limit) : scored;
|
|
3788
|
+
if (output.length === 0) return output;
|
|
3789
|
+
const delivery = advisory_delivery(graph, output.map((entry) => entry.task), options);
|
|
3790
|
+
const ledger = build_capacity_ledger(delivery, graph.resources, options.today ?? /* @__PURE__ */ new Date());
|
|
3791
|
+
for (const entry of output) {
|
|
3792
|
+
entry.fits_capacity = check_capacity(entry.task, ledger);
|
|
3793
|
+
entry.fits_wip = check_wip(
|
|
3794
|
+
graph.tracks.get(entry.task.track ?? "unassigned"),
|
|
3795
|
+
graph
|
|
3796
|
+
);
|
|
848
3797
|
}
|
|
849
3798
|
return output;
|
|
850
3799
|
}
|
|
851
3800
|
|
|
852
|
-
// src/planning/
|
|
853
|
-
|
|
854
|
-
|
|
3801
|
+
// src/planning/velocity.ts
|
|
3802
|
+
var import_node_fs11 = __toESM(require("fs"), 1);
|
|
3803
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
3804
|
+
var DEFAULT_ESTIMATION_CONFIG2 = {
|
|
3805
|
+
version: 2,
|
|
3806
|
+
project: {
|
|
3807
|
+
name: "COOP",
|
|
3808
|
+
id: "coop"
|
|
3809
|
+
},
|
|
3810
|
+
defaults: {
|
|
3811
|
+
task: {
|
|
3812
|
+
complexity: "medium"
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
};
|
|
3816
|
+
function asDate5(value) {
|
|
3817
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
3818
|
+
const parsed = new Date(value);
|
|
3819
|
+
if (Number.isNaN(parsed.valueOf())) {
|
|
3820
|
+
throw new Error(`Invalid date '${value}'.`);
|
|
3821
|
+
}
|
|
3822
|
+
return parsed;
|
|
855
3823
|
}
|
|
856
|
-
function
|
|
857
|
-
|
|
858
|
-
|
|
3824
|
+
function isoDate4(value) {
|
|
3825
|
+
return value.toISOString().slice(0, 10);
|
|
3826
|
+
}
|
|
3827
|
+
function startOfWeek3(date) {
|
|
3828
|
+
const copy = new Date(date.getTime());
|
|
3829
|
+
const day = copy.getUTCDay();
|
|
3830
|
+
const offset = day === 0 ? -6 : 1 - day;
|
|
3831
|
+
copy.setUTCDate(copy.getUTCDate() + offset);
|
|
3832
|
+
copy.setUTCHours(0, 0, 0, 0);
|
|
3833
|
+
return copy;
|
|
3834
|
+
}
|
|
3835
|
+
function addDays4(date, days) {
|
|
3836
|
+
const out = new Date(date.getTime());
|
|
3837
|
+
out.setUTCDate(out.getUTCDate() + days);
|
|
3838
|
+
return out;
|
|
3839
|
+
}
|
|
3840
|
+
function durationHours2(run) {
|
|
3841
|
+
const fromSteps = run.steps.reduce((sum, step) => sum + (step.duration_seconds ?? 0), 0) / 3600;
|
|
3842
|
+
if (fromSteps > 0) {
|
|
3843
|
+
return fromSteps;
|
|
859
3844
|
}
|
|
860
|
-
if (
|
|
861
|
-
|
|
3845
|
+
if (run.completed) {
|
|
3846
|
+
const started = asDate5(run.started);
|
|
3847
|
+
const completed = asDate5(run.completed);
|
|
3848
|
+
return Math.max(0, completed.getTime() - started.getTime()) / (60 * 60 * 1e3);
|
|
862
3849
|
}
|
|
863
|
-
|
|
864
|
-
|
|
3850
|
+
return 0;
|
|
3851
|
+
}
|
|
3852
|
+
function mean2(values) {
|
|
3853
|
+
if (values.length === 0) return 0;
|
|
3854
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
3855
|
+
}
|
|
3856
|
+
function detectTrend(points) {
|
|
3857
|
+
if (points.length < 4) {
|
|
3858
|
+
return "stable";
|
|
865
3859
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
3860
|
+
const midpoint = Math.floor(points.length / 2);
|
|
3861
|
+
const firstHalf = points.slice(0, midpoint).map((point) => point.completed_tasks);
|
|
3862
|
+
const secondHalf = points.slice(midpoint).map((point) => point.completed_tasks);
|
|
3863
|
+
const firstAverage = mean2(firstHalf);
|
|
3864
|
+
const secondAverage = mean2(secondHalf);
|
|
3865
|
+
if (firstAverage === 0 && secondAverage === 0) {
|
|
3866
|
+
return "stable";
|
|
871
3867
|
}
|
|
872
|
-
|
|
3868
|
+
if (firstAverage === 0 && secondAverage > 0) {
|
|
3869
|
+
return "accelerating";
|
|
3870
|
+
}
|
|
3871
|
+
const ratio = secondAverage / Math.max(firstAverage, 1e-4);
|
|
3872
|
+
if (ratio >= 1.15) {
|
|
3873
|
+
return "accelerating";
|
|
3874
|
+
}
|
|
3875
|
+
if (ratio <= 0.85) {
|
|
3876
|
+
return "decelerating";
|
|
3877
|
+
}
|
|
3878
|
+
return "stable";
|
|
873
3879
|
}
|
|
874
|
-
function
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
3880
|
+
function load_completed_runs(coopDir) {
|
|
3881
|
+
const runsDir = import_node_path5.default.join(coopDir, "runs");
|
|
3882
|
+
if (!import_node_fs11.default.existsSync(runsDir)) {
|
|
3883
|
+
return [];
|
|
878
3884
|
}
|
|
879
|
-
return
|
|
3885
|
+
return import_node_fs11.default.readdirSync(runsDir).filter((entry) => entry.toLowerCase().endsWith(".yml") || entry.toLowerCase().endsWith(".yaml")).sort((a, b) => a.localeCompare(b)).map((entry) => parseYamlFile(import_node_path5.default.join(runsDir, entry))).filter((run) => run.status === "completed" && typeof run.completed === "string");
|
|
880
3886
|
}
|
|
881
|
-
function
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
3887
|
+
function compute_velocity(runs, windowWeeks, options = {}) {
|
|
3888
|
+
const normalizedWindow = Number.isInteger(windowWeeks) && windowWeeks > 0 ? windowWeeks : 4;
|
|
3889
|
+
const today = startOfWeek3(asDate5(options.today ?? /* @__PURE__ */ new Date()));
|
|
3890
|
+
const windowStart = addDays4(today, -7 * (normalizedWindow - 1));
|
|
3891
|
+
const pointMap = /* @__PURE__ */ new Map();
|
|
3892
|
+
for (let weekIndex = 0; weekIndex < normalizedWindow; weekIndex += 1) {
|
|
3893
|
+
const weekStart = addDays4(windowStart, weekIndex * 7);
|
|
3894
|
+
const weekEnd = addDays4(weekStart, 6);
|
|
3895
|
+
pointMap.set(isoDate4(weekStart), {
|
|
3896
|
+
tasks: /* @__PURE__ */ new Set(),
|
|
3897
|
+
hours: 0,
|
|
3898
|
+
index: weekIndex,
|
|
3899
|
+
end: isoDate4(weekEnd)
|
|
3900
|
+
});
|
|
3901
|
+
}
|
|
3902
|
+
let completedRuns = 0;
|
|
3903
|
+
let estimatedHours = 0;
|
|
3904
|
+
let actualHours = 0;
|
|
3905
|
+
for (const run of runs) {
|
|
3906
|
+
if (!run.completed) continue;
|
|
3907
|
+
const completedAt = asDate5(run.completed);
|
|
3908
|
+
const weekStart = startOfWeek3(completedAt);
|
|
3909
|
+
const weekKey = isoDate4(weekStart);
|
|
3910
|
+
const bucket = pointMap.get(weekKey);
|
|
3911
|
+
if (!bucket) continue;
|
|
3912
|
+
completedRuns += 1;
|
|
3913
|
+
const hours = durationHours2(run);
|
|
3914
|
+
actualHours += hours;
|
|
3915
|
+
bucket.tasks.add(run.task);
|
|
3916
|
+
bucket.hours += hours;
|
|
3917
|
+
const task = options.graph?.nodes.get(run.task);
|
|
3918
|
+
if (task) {
|
|
3919
|
+
estimatedHours += effort_or_default(task, DEFAULT_ESTIMATION_CONFIG2);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
const points = Array.from(pointMap.entries()).sort((a, b) => a[1].index - b[1].index).map(([weekStart, bucket]) => ({
|
|
3923
|
+
week_index: bucket.index,
|
|
3924
|
+
week_start: weekStart,
|
|
3925
|
+
week_end: bucket.end,
|
|
3926
|
+
completed_tasks: bucket.tasks.size,
|
|
3927
|
+
delivered_hours: Number(bucket.hours.toFixed(2))
|
|
3928
|
+
}));
|
|
3929
|
+
const tasksCompletedTotal = points.reduce((sum, point) => sum + point.completed_tasks, 0);
|
|
3930
|
+
const deliveredHoursTotal = points.reduce((sum, point) => sum + point.delivered_hours, 0);
|
|
3931
|
+
return {
|
|
3932
|
+
window_weeks: normalizedWindow,
|
|
3933
|
+
completed_runs: completedRuns,
|
|
3934
|
+
tasks_completed_total: tasksCompletedTotal,
|
|
3935
|
+
delivered_hours_total: Number(deliveredHoursTotal.toFixed(2)),
|
|
3936
|
+
tasks_completed_per_week: Number((tasksCompletedTotal / normalizedWindow).toFixed(2)),
|
|
3937
|
+
hours_delivered_per_week: Number((deliveredHoursTotal / normalizedWindow).toFixed(2)),
|
|
3938
|
+
accuracy_ratio: estimatedHours > 0 ? Number((actualHours / estimatedHours).toFixed(3)) : null,
|
|
3939
|
+
trend: detectTrend(points),
|
|
3940
|
+
points
|
|
888
3941
|
};
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
// src/planning/what-if.ts
|
|
3945
|
+
function asDate6(value) {
|
|
3946
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
3947
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
3948
|
+
if (Number.isNaN(parsed.valueOf())) {
|
|
3949
|
+
throw new Error(`Invalid date '${value}'.`);
|
|
892
3950
|
}
|
|
893
|
-
return
|
|
3951
|
+
return parsed;
|
|
894
3952
|
}
|
|
895
|
-
function
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
3953
|
+
function calendar_days2(from, to) {
|
|
3954
|
+
const start = asDate6(from);
|
|
3955
|
+
const end = asDate6(to);
|
|
3956
|
+
return Math.round((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1e3));
|
|
3957
|
+
}
|
|
3958
|
+
function signed_number(value, suffix = "") {
|
|
3959
|
+
if (!Number.isFinite(value) || value === 0) return "-";
|
|
3960
|
+
return `${value > 0 ? "+" : ""}${value}${suffix}`;
|
|
3961
|
+
}
|
|
3962
|
+
function signed_hours(value) {
|
|
3963
|
+
if (!Number.isFinite(value) || value === 0) return "-";
|
|
3964
|
+
return `${value > 0 ? "+" : ""}${value.toFixed(1)}h`;
|
|
3965
|
+
}
|
|
3966
|
+
function format_hours(value) {
|
|
3967
|
+
if (!Number.isFinite(value)) return "unbounded";
|
|
3968
|
+
return `${value.toFixed(1)}h`;
|
|
3969
|
+
}
|
|
3970
|
+
function format_headroom(result) {
|
|
3971
|
+
const budget = result.summary.effort_hours.budget;
|
|
3972
|
+
if (!Number.isFinite(budget)) return "unbounded";
|
|
3973
|
+
return `${(budget - result.summary.effort_hours.required).toFixed(1)}h`;
|
|
3974
|
+
}
|
|
3975
|
+
function modification_label(modification) {
|
|
3976
|
+
switch (modification.kind) {
|
|
3977
|
+
case "without":
|
|
3978
|
+
return `--without ${modification.task_id}`;
|
|
3979
|
+
case "add_member":
|
|
3980
|
+
return `--add-member ${modification.target}`;
|
|
3981
|
+
case "target":
|
|
3982
|
+
return `--target ${modification.target_date}`;
|
|
3983
|
+
case "set_priority":
|
|
3984
|
+
return `--set ${modification.task_id}:priority=${modification.priority}`;
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
function build_rows(baseline, scenario) {
|
|
3988
|
+
const baselineTarget = baseline.summary.dates.target ?? "-";
|
|
3989
|
+
const scenarioTarget = scenario.summary.dates.target ?? "-";
|
|
3990
|
+
const baselineProjected = baseline.summary.dates.projected ?? "-";
|
|
3991
|
+
const scenarioProjected = scenario.summary.dates.projected ?? "-";
|
|
3992
|
+
const baselineHeadroom = Number.isFinite(baseline.summary.effort_hours.budget) ? baseline.summary.effort_hours.budget - baseline.summary.effort_hours.required : Number.NaN;
|
|
3993
|
+
const scenarioHeadroom = Number.isFinite(scenario.summary.effort_hours.budget) ? scenario.summary.effort_hours.budget - scenario.summary.effort_hours.required : Number.NaN;
|
|
3994
|
+
return [
|
|
3995
|
+
{
|
|
3996
|
+
metric: "Target date",
|
|
3997
|
+
original: baselineTarget,
|
|
3998
|
+
scenario: scenarioTarget,
|
|
3999
|
+
delta: baseline.summary.dates.target && scenario.summary.dates.target ? signed_number(calendar_days2(baseline.summary.dates.target, scenario.summary.dates.target), " days") : "-"
|
|
4000
|
+
},
|
|
4001
|
+
{
|
|
4002
|
+
metric: "Projected date",
|
|
4003
|
+
original: baselineProjected,
|
|
4004
|
+
scenario: scenarioProjected,
|
|
4005
|
+
delta: baseline.summary.dates.projected && scenario.summary.dates.projected ? signed_number(
|
|
4006
|
+
calendar_days2(baseline.summary.dates.projected, scenario.summary.dates.projected),
|
|
4007
|
+
" days"
|
|
4008
|
+
) : "-"
|
|
4009
|
+
},
|
|
4010
|
+
{
|
|
4011
|
+
metric: "Effort (hours)",
|
|
4012
|
+
original: format_hours(baseline.summary.effort_hours.required),
|
|
4013
|
+
scenario: format_hours(scenario.summary.effort_hours.required),
|
|
4014
|
+
delta: signed_hours(scenario.summary.effort_hours.required - baseline.summary.effort_hours.required)
|
|
4015
|
+
},
|
|
4016
|
+
{
|
|
4017
|
+
metric: "Headroom",
|
|
4018
|
+
original: format_headroom(baseline),
|
|
4019
|
+
scenario: format_headroom(scenario),
|
|
4020
|
+
delta: signed_hours(scenarioHeadroom - baselineHeadroom)
|
|
4021
|
+
},
|
|
4022
|
+
{
|
|
4023
|
+
metric: "Critical path tasks",
|
|
4024
|
+
original: String(baseline.summary.critical_path.length),
|
|
4025
|
+
scenario: String(scenario.summary.critical_path.length),
|
|
4026
|
+
delta: signed_number(scenario.summary.critical_path.length - baseline.summary.critical_path.length)
|
|
4027
|
+
},
|
|
4028
|
+
{
|
|
4029
|
+
metric: "Status",
|
|
4030
|
+
original: baseline.status,
|
|
4031
|
+
scenario: scenario.status,
|
|
4032
|
+
delta: baseline.status === scenario.status ? "-" : `${baseline.status} -> ${scenario.status}`
|
|
918
4033
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
4034
|
+
];
|
|
4035
|
+
}
|
|
4036
|
+
function clone_graph(graph) {
|
|
4037
|
+
return structuredClone(graph);
|
|
4038
|
+
}
|
|
4039
|
+
function resolve_delivery(graph, deliveryId) {
|
|
4040
|
+
const delivery = graph.deliveries.get(deliveryId);
|
|
4041
|
+
if (!delivery) {
|
|
4042
|
+
throw new Error(`Delivery '${deliveryId}' not found.`);
|
|
4043
|
+
}
|
|
4044
|
+
return delivery;
|
|
4045
|
+
}
|
|
4046
|
+
function remove_task_and_exclusive_dependencies(graph, delivery, taskId) {
|
|
4047
|
+
const scoped = new Set(delivery.scope.include);
|
|
4048
|
+
for (const excluded of delivery.scope.exclude) {
|
|
4049
|
+
scoped.delete(excluded);
|
|
4050
|
+
}
|
|
4051
|
+
if (!scoped.has(taskId)) {
|
|
4052
|
+
throw new Error(`Task '${taskId}' is not in delivery '${delivery.id}' scope.`);
|
|
4053
|
+
}
|
|
4054
|
+
const toRemove = /* @__PURE__ */ new Set([taskId]);
|
|
4055
|
+
const queue = [taskId];
|
|
4056
|
+
while (queue.length > 0) {
|
|
4057
|
+
const current = queue.shift();
|
|
4058
|
+
if (!current) continue;
|
|
4059
|
+
for (const dependencyId of graph.forward.get(current) ?? /* @__PURE__ */ new Set()) {
|
|
4060
|
+
if (!scoped.has(dependencyId) || toRemove.has(dependencyId)) continue;
|
|
4061
|
+
const dependents = graph.reverse.get(dependencyId) ?? /* @__PURE__ */ new Set();
|
|
4062
|
+
const hasRemainingDependent = Array.from(dependents).some(
|
|
4063
|
+
(dependentId) => scoped.has(dependentId) && !toRemove.has(dependentId)
|
|
4064
|
+
);
|
|
4065
|
+
if (hasRemainingDependent) continue;
|
|
4066
|
+
toRemove.add(dependencyId);
|
|
4067
|
+
queue.push(dependencyId);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
delivery.scope.include = delivery.scope.include.filter((id) => !toRemove.has(id));
|
|
4071
|
+
delivery.scope.exclude = delivery.scope.exclude.filter((id) => !toRemove.has(id));
|
|
4072
|
+
}
|
|
4073
|
+
function resolve_human_profile(graph, delivery, target) {
|
|
4074
|
+
const direct = graph.resources.get(target);
|
|
4075
|
+
if (direct?.type === "human") {
|
|
4076
|
+
return direct;
|
|
4077
|
+
}
|
|
4078
|
+
const track = graph.tracks.get(target);
|
|
4079
|
+
if (track) {
|
|
4080
|
+
for (const profileId of track.capacity_profiles) {
|
|
4081
|
+
const profile = graph.resources.get(profileId);
|
|
4082
|
+
if (profile?.type === "human") {
|
|
4083
|
+
return profile;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
for (const profileId of delivery.capacity_profiles) {
|
|
4088
|
+
const profile = graph.resources.get(profileId);
|
|
4089
|
+
if (profile?.type === "human") {
|
|
4090
|
+
return profile;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
throw new Error(`No human capacity profile found for '${target}'.`);
|
|
4094
|
+
}
|
|
4095
|
+
function add_member(graph, delivery, target, hoursPerWeek) {
|
|
4096
|
+
const profile = resolve_human_profile(graph, delivery, target);
|
|
4097
|
+
const defaultHours = hoursPerWeek ?? profile.defaults?.hours_per_week ?? profile.members[0]?.hours_per_week ?? 40;
|
|
4098
|
+
const nextMemberIndex = profile.members.length + 1;
|
|
4099
|
+
profile.members = [
|
|
4100
|
+
...profile.members,
|
|
4101
|
+
{
|
|
4102
|
+
id: `what-if-${profile.id}-${nextMemberIndex}`,
|
|
4103
|
+
hours_per_week: defaultHours
|
|
929
4104
|
}
|
|
4105
|
+
];
|
|
4106
|
+
graph.resources.set(profile.id, profile);
|
|
4107
|
+
}
|
|
4108
|
+
function set_target(delivery, targetDate) {
|
|
4109
|
+
asDate6(targetDate);
|
|
4110
|
+
delivery.target_date = targetDate;
|
|
4111
|
+
}
|
|
4112
|
+
function set_priority(graph, taskId, priority) {
|
|
4113
|
+
const task = graph.nodes.get(taskId);
|
|
4114
|
+
if (!task) {
|
|
4115
|
+
throw new Error(`Task '${taskId}' not found.`);
|
|
4116
|
+
}
|
|
4117
|
+
task.priority = priority;
|
|
4118
|
+
graph.nodes.set(taskId, task);
|
|
4119
|
+
}
|
|
4120
|
+
function apply_modification(graph, deliveryId, modification) {
|
|
4121
|
+
const delivery = resolve_delivery(graph, deliveryId);
|
|
4122
|
+
switch (modification.kind) {
|
|
4123
|
+
case "without":
|
|
4124
|
+
remove_task_and_exclusive_dependencies(graph, delivery, modification.task_id);
|
|
4125
|
+
return;
|
|
4126
|
+
case "add_member":
|
|
4127
|
+
add_member(graph, delivery, modification.target, modification.hours_per_week);
|
|
4128
|
+
return;
|
|
4129
|
+
case "target":
|
|
4130
|
+
set_target(delivery, modification.target_date);
|
|
4131
|
+
graph.deliveries.set(delivery.id, delivery);
|
|
4132
|
+
return;
|
|
4133
|
+
case "set_priority":
|
|
4134
|
+
set_priority(graph, modification.task_id, modification.priority);
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
function analyze_what_if(baseline, modification) {
|
|
4139
|
+
const modifications = Array.isArray(modification) ? modification : [modification];
|
|
4140
|
+
if (modifications.length === 0) {
|
|
4141
|
+
throw new Error("At least one what-if modification is required.");
|
|
4142
|
+
}
|
|
4143
|
+
const baselineResult = analyze_feasibility(baseline.delivery_id, baseline.graph, baseline.today);
|
|
4144
|
+
const scenarioGraph = clone_graph(baseline.graph);
|
|
4145
|
+
for (const next of modifications) {
|
|
4146
|
+
apply_modification(scenarioGraph, baseline.delivery_id, next);
|
|
930
4147
|
}
|
|
4148
|
+
const scenarioResult = analyze_feasibility(baseline.delivery_id, scenarioGraph, baseline.today);
|
|
931
4149
|
return {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
4150
|
+
delivery: baseline.delivery_id,
|
|
4151
|
+
label: modifications.map(modification_label).join(" "),
|
|
4152
|
+
baseline: baselineResult,
|
|
4153
|
+
scenario: scenarioResult,
|
|
4154
|
+
rows: build_rows(baselineResult, scenarioResult)
|
|
936
4155
|
};
|
|
937
4156
|
}
|
|
938
4157
|
|
|
4158
|
+
// src/schema/migration.ts
|
|
4159
|
+
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
4160
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
4161
|
+
|
|
939
4162
|
// src/schema/version.ts
|
|
940
|
-
var
|
|
941
|
-
var
|
|
4163
|
+
var import_node_fs12 = __toESM(require("fs"), 1);
|
|
4164
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
942
4165
|
var CURRENT_SCHEMA_VERSION = 2;
|
|
943
4166
|
function schemaVersionFile(coopDir) {
|
|
944
|
-
return
|
|
4167
|
+
return import_node_path6.default.join(coopDir, "schema-version");
|
|
945
4168
|
}
|
|
946
4169
|
function read_schema_version(coopDir) {
|
|
947
4170
|
const filePath = schemaVersionFile(coopDir);
|
|
948
|
-
if (!
|
|
4171
|
+
if (!import_node_fs12.default.existsSync(filePath)) {
|
|
949
4172
|
throw new Error(`Missing schema-version file at ${filePath}.`);
|
|
950
4173
|
}
|
|
951
|
-
const raw =
|
|
4174
|
+
const raw = import_node_fs12.default.readFileSync(filePath, "utf8").trim();
|
|
952
4175
|
if (!raw) {
|
|
953
4176
|
throw new Error(`Schema version file is empty at ${filePath}.`);
|
|
954
4177
|
}
|
|
@@ -963,10 +4186,168 @@ function write_schema_version(coopDir, version) {
|
|
|
963
4186
|
throw new Error(`Schema version must be a positive integer. Received: ${String(version)}.`);
|
|
964
4187
|
}
|
|
965
4188
|
const filePath = schemaVersionFile(coopDir);
|
|
966
|
-
|
|
4189
|
+
import_node_fs12.default.writeFileSync(filePath, `${version}
|
|
967
4190
|
`, "utf8");
|
|
968
4191
|
}
|
|
969
4192
|
|
|
4193
|
+
// src/schema/migration.ts
|
|
4194
|
+
function asString2(value) {
|
|
4195
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
4196
|
+
}
|
|
4197
|
+
function asRecord(value) {
|
|
4198
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
4199
|
+
}
|
|
4200
|
+
function cloneRaw(raw) {
|
|
4201
|
+
return JSON.parse(JSON.stringify(raw));
|
|
4202
|
+
}
|
|
4203
|
+
function estimate_to_three_point(value) {
|
|
4204
|
+
return {
|
|
4205
|
+
optimistic_hours: Number((value * 0.7).toFixed(2)),
|
|
4206
|
+
expected_hours: Number(value.toFixed(2)),
|
|
4207
|
+
pessimistic_hours: Number((value * 1.8).toFixed(2))
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
function walk_files(dirPath, extensions) {
|
|
4211
|
+
if (!import_node_fs13.default.existsSync(dirPath)) return [];
|
|
4212
|
+
const out = [];
|
|
4213
|
+
const entries = import_node_fs13.default.readdirSync(dirPath, { withFileTypes: true });
|
|
4214
|
+
for (const entry of entries) {
|
|
4215
|
+
const fullPath = import_node_path7.default.join(dirPath, entry.name);
|
|
4216
|
+
if (entry.isDirectory()) {
|
|
4217
|
+
out.push(...walk_files(fullPath, extensions));
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
if (!entry.isFile()) continue;
|
|
4221
|
+
const ext = import_node_path7.default.extname(entry.name).toLowerCase();
|
|
4222
|
+
if (extensions.has(ext)) {
|
|
4223
|
+
out.push(fullPath);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
4227
|
+
}
|
|
4228
|
+
function file_mtime_iso(filePath) {
|
|
4229
|
+
const stats = import_node_fs13.default.statSync(filePath);
|
|
4230
|
+
return stats.mtime.toISOString().slice(0, 10);
|
|
4231
|
+
}
|
|
4232
|
+
function migration_v1_to_v2(rawTask, context = {}) {
|
|
4233
|
+
const migrated = { ...rawTask };
|
|
4234
|
+
if (migrated.status === "dropped") {
|
|
4235
|
+
migrated.status = "canceled";
|
|
4236
|
+
}
|
|
4237
|
+
if (migrated.type === "task") {
|
|
4238
|
+
migrated.type = "chore";
|
|
4239
|
+
} else if (migrated.type === "research") {
|
|
4240
|
+
migrated.type = "spike";
|
|
4241
|
+
}
|
|
4242
|
+
if (!Object.prototype.hasOwnProperty.call(migrated, "updated")) {
|
|
4243
|
+
migrated.updated = context.file_mtime_iso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4244
|
+
}
|
|
4245
|
+
if (!Object.prototype.hasOwnProperty.call(migrated, "assignee")) {
|
|
4246
|
+
migrated.assignee = null;
|
|
4247
|
+
}
|
|
4248
|
+
const deliveryRecord = asRecord(migrated.delivery);
|
|
4249
|
+
if (deliveryRecord) {
|
|
4250
|
+
const include = deliveryRecord.include;
|
|
4251
|
+
if (Array.isArray(include)) {
|
|
4252
|
+
const first = include.find((entry) => typeof entry === "string");
|
|
4253
|
+
migrated.delivery = asString2(first) ?? null;
|
|
4254
|
+
} else if (typeof include === "string" && include.trim().length > 0) {
|
|
4255
|
+
migrated.delivery = include.trim();
|
|
4256
|
+
} else {
|
|
4257
|
+
migrated.delivery = null;
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
const estimate = migrated.estimate;
|
|
4261
|
+
if (typeof estimate === "number" && Number.isFinite(estimate) && estimate > 0) {
|
|
4262
|
+
migrated.estimate = estimate_to_three_point(estimate);
|
|
4263
|
+
} else if (asRecord(estimate)) {
|
|
4264
|
+
const estimateRecord = estimate;
|
|
4265
|
+
const hasExpectedHours = typeof estimateRecord.expected_hours === "number" && Number.isFinite(estimateRecord.expected_hours);
|
|
4266
|
+
const hasOptimisticHours = typeof estimateRecord.optimistic_hours === "number" && Number.isFinite(estimateRecord.optimistic_hours);
|
|
4267
|
+
const hasPessimisticHours = typeof estimateRecord.pessimistic_hours === "number" && Number.isFinite(estimateRecord.pessimistic_hours);
|
|
4268
|
+
if (hasExpectedHours && (!hasOptimisticHours || !hasPessimisticHours)) {
|
|
4269
|
+
migrated.estimate = {
|
|
4270
|
+
...estimateRecord,
|
|
4271
|
+
optimistic_hours: hasOptimisticHours ? estimateRecord.optimistic_hours : Number((Number(estimateRecord.expected_hours) * 0.7).toFixed(2)),
|
|
4272
|
+
pessimistic_hours: hasPessimisticHours ? estimateRecord.pessimistic_hours : Number((Number(estimateRecord.expected_hours) * 1.8).toFixed(2))
|
|
4273
|
+
};
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
return migrated;
|
|
4277
|
+
}
|
|
4278
|
+
var MIGRATIONS = [
|
|
4279
|
+
{
|
|
4280
|
+
from: 1,
|
|
4281
|
+
to: 2,
|
|
4282
|
+
description: "COOP schema v1 to v2 task migration.",
|
|
4283
|
+
migrate: migration_v1_to_v2
|
|
4284
|
+
}
|
|
4285
|
+
];
|
|
4286
|
+
function migration_for_step(fromVersion, toVersion) {
|
|
4287
|
+
const found = MIGRATIONS.find((migration) => migration.from === fromVersion && migration.to === toVersion);
|
|
4288
|
+
if (!found) {
|
|
4289
|
+
throw new Error(`No schema migration registered for v${fromVersion} -> v${toVersion}.`);
|
|
4290
|
+
}
|
|
4291
|
+
return found;
|
|
4292
|
+
}
|
|
4293
|
+
function migrate_task(rawTask, fromVersion, toVersion, context = {}) {
|
|
4294
|
+
if (!Number.isInteger(fromVersion) || fromVersion <= 0) {
|
|
4295
|
+
throw new Error(`Invalid fromVersion '${String(fromVersion)}'.`);
|
|
4296
|
+
}
|
|
4297
|
+
if (!Number.isInteger(toVersion) || toVersion <= 0) {
|
|
4298
|
+
throw new Error(`Invalid toVersion '${String(toVersion)}'.`);
|
|
4299
|
+
}
|
|
4300
|
+
if (fromVersion > toVersion) {
|
|
4301
|
+
throw new Error(`Downgrade migration is not supported: v${fromVersion} -> v${toVersion}.`);
|
|
4302
|
+
}
|
|
4303
|
+
if (fromVersion === toVersion) {
|
|
4304
|
+
return cloneRaw(rawTask);
|
|
4305
|
+
}
|
|
4306
|
+
let current = cloneRaw(rawTask);
|
|
4307
|
+
for (let version = fromVersion; version < toVersion; version += 1) {
|
|
4308
|
+
const migration = migration_for_step(version, version + 1);
|
|
4309
|
+
current = migration.migrate(current, context);
|
|
4310
|
+
}
|
|
4311
|
+
return current;
|
|
4312
|
+
}
|
|
4313
|
+
function migrate_repository(coopDir, targetVersion = CURRENT_SCHEMA_VERSION, options = {}) {
|
|
4314
|
+
if (!Number.isInteger(targetVersion) || targetVersion <= 0) {
|
|
4315
|
+
throw new Error(`Target version must be a positive integer. Received: ${String(targetVersion)}.`);
|
|
4316
|
+
}
|
|
4317
|
+
const dry_run = Boolean(options.dry_run);
|
|
4318
|
+
const from_version = read_schema_version(coopDir);
|
|
4319
|
+
if (from_version > targetVersion) {
|
|
4320
|
+
throw new Error(`Cannot migrate backwards from v${from_version} to v${targetVersion}.`);
|
|
4321
|
+
}
|
|
4322
|
+
const taskFiles = walk_files(import_node_path7.default.join(coopDir, "tasks"), /* @__PURE__ */ new Set([".md"]));
|
|
4323
|
+
const changed_files = [];
|
|
4324
|
+
for (const filePath of taskFiles) {
|
|
4325
|
+
const { frontmatter, body } = parseFrontmatterFile(filePath);
|
|
4326
|
+
const migrated = migrate_task(frontmatter, from_version, targetVersion, {
|
|
4327
|
+
file_path: filePath,
|
|
4328
|
+
file_mtime_iso: file_mtime_iso(filePath)
|
|
4329
|
+
});
|
|
4330
|
+
const before = JSON.stringify(frontmatter);
|
|
4331
|
+
const after = JSON.stringify(migrated);
|
|
4332
|
+
if (before === after) continue;
|
|
4333
|
+
changed_files.push(filePath);
|
|
4334
|
+
if (!dry_run) {
|
|
4335
|
+
import_node_fs13.default.writeFileSync(filePath, stringifyFrontmatter(migrated, body), "utf8");
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
if (!dry_run && from_version !== targetVersion) {
|
|
4339
|
+
write_schema_version(coopDir, targetVersion);
|
|
4340
|
+
}
|
|
4341
|
+
return {
|
|
4342
|
+
from_version,
|
|
4343
|
+
to_version: targetVersion,
|
|
4344
|
+
dry_run,
|
|
4345
|
+
files_scanned: taskFiles.length,
|
|
4346
|
+
files_changed: changed_files.length,
|
|
4347
|
+
changed_files
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
|
|
970
4351
|
// src/state/auto-transitions.ts
|
|
971
4352
|
function toStatusMap(value) {
|
|
972
4353
|
if (!value) return /* @__PURE__ */ new Map();
|
|
@@ -1044,6 +4425,14 @@ function toIsoDate(now) {
|
|
|
1044
4425
|
const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
|
|
1045
4426
|
return date.toISOString().slice(0, 10);
|
|
1046
4427
|
}
|
|
4428
|
+
function toIsoTimestamp2(now) {
|
|
4429
|
+
if (typeof now === "string") {
|
|
4430
|
+
if (now.includes("T")) return now;
|
|
4431
|
+
return `${now}T00:00:00.000Z`;
|
|
4432
|
+
}
|
|
4433
|
+
const date = now instanceof Date ? now : /* @__PURE__ */ new Date();
|
|
4434
|
+
return date.toISOString();
|
|
4435
|
+
}
|
|
1047
4436
|
function validateGovernanceAndDeps(task, targetStatus, context) {
|
|
1048
4437
|
if (task.status === "in_review" && targetStatus === "done" && task.governance?.approval_required) {
|
|
1049
4438
|
const reviewer = task.governance.reviewer;
|
|
@@ -1080,13 +4469,26 @@ function transition(task, targetStatus, context = {}) {
|
|
|
1080
4469
|
error: governanceError
|
|
1081
4470
|
};
|
|
1082
4471
|
}
|
|
4472
|
+
const nextTask = {
|
|
4473
|
+
...task,
|
|
4474
|
+
status: targetStatus,
|
|
4475
|
+
updated: toIsoDate(context.now)
|
|
4476
|
+
};
|
|
4477
|
+
if (targetStatus !== task.status) {
|
|
4478
|
+
context.eventEmitter?.emit({
|
|
4479
|
+
type: "task.transitioned",
|
|
4480
|
+
timestamp: toIsoTimestamp2(context.now),
|
|
4481
|
+
payload: {
|
|
4482
|
+
task_id: task.id,
|
|
4483
|
+
from: task.status,
|
|
4484
|
+
to: targetStatus,
|
|
4485
|
+
actor: context.actor
|
|
4486
|
+
}
|
|
4487
|
+
});
|
|
4488
|
+
}
|
|
1083
4489
|
return {
|
|
1084
4490
|
success: true,
|
|
1085
|
-
task:
|
|
1086
|
-
...task,
|
|
1087
|
-
status: targetStatus,
|
|
1088
|
-
updated: toIsoDate(context.now)
|
|
1089
|
-
}
|
|
4491
|
+
task: nextTask
|
|
1090
4492
|
};
|
|
1091
4493
|
}
|
|
1092
4494
|
|
|
@@ -1304,8 +4706,9 @@ function validateSemantic(task, context = {}) {
|
|
|
1304
4706
|
}
|
|
1305
4707
|
|
|
1306
4708
|
// src/validator/structural.ts
|
|
1307
|
-
var
|
|
1308
|
-
var ID_PATTERN = /^[A-
|
|
4709
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
4710
|
+
var ID_PATTERN = /^[A-Z0-9]+(?:-[A-Z0-9]+)+$/;
|
|
4711
|
+
var ALIAS_PATTERN = /^[A-Z0-9]+(?:[.-][A-Z0-9]+)*$/;
|
|
1309
4712
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1310
4713
|
function error4(field, rule, message) {
|
|
1311
4714
|
return { level: "error", field, rule, message };
|
|
@@ -1347,13 +4750,43 @@ function validateStructural(task, context = {}) {
|
|
|
1347
4750
|
errors.push(error4("updated", "struct.updated_iso_date", `Field 'updated' must be an ISO date (YYYY-MM-DD).`));
|
|
1348
4751
|
}
|
|
1349
4752
|
if (context.filePath) {
|
|
1350
|
-
const expected =
|
|
4753
|
+
const expected = import_node_path8.default.basename(context.filePath, import_node_path8.default.extname(context.filePath));
|
|
1351
4754
|
if (typeof task.id === "string" && task.id !== expected) {
|
|
1352
4755
|
errors.push(
|
|
1353
4756
|
error4("id", "struct.id_matches_filename", `Task id '${task.id}' must match filename '${expected}'.`)
|
|
1354
4757
|
);
|
|
1355
4758
|
}
|
|
1356
4759
|
}
|
|
4760
|
+
const aliases = task.aliases;
|
|
4761
|
+
if (aliases !== void 0) {
|
|
4762
|
+
if (!Array.isArray(aliases)) {
|
|
4763
|
+
errors.push(error4("aliases", "struct.aliases_array", "Field 'aliases' must be an array of strings."));
|
|
4764
|
+
} else {
|
|
4765
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4766
|
+
for (const rawAlias of aliases) {
|
|
4767
|
+
if (typeof rawAlias !== "string" || rawAlias.trim().length === 0) {
|
|
4768
|
+
errors.push(error4("aliases", "struct.aliases_string", "Field 'aliases' entries must be non-empty strings."));
|
|
4769
|
+
continue;
|
|
4770
|
+
}
|
|
4771
|
+
const normalized = rawAlias.trim().toUpperCase().replace(/_/g, ".");
|
|
4772
|
+
if (!ALIAS_PATTERN.test(normalized)) {
|
|
4773
|
+
errors.push(
|
|
4774
|
+
error4(
|
|
4775
|
+
"aliases",
|
|
4776
|
+
"struct.aliases_pattern",
|
|
4777
|
+
`Alias '${rawAlias}' is invalid. Use letters/numbers with '.' or '-' separators.`
|
|
4778
|
+
)
|
|
4779
|
+
);
|
|
4780
|
+
continue;
|
|
4781
|
+
}
|
|
4782
|
+
if (seen.has(normalized)) {
|
|
4783
|
+
errors.push(error4("aliases", "struct.aliases_unique", `Alias '${rawAlias}' is duplicated.`));
|
|
4784
|
+
continue;
|
|
4785
|
+
}
|
|
4786
|
+
seen.add(normalized);
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
1357
4790
|
return errors;
|
|
1358
4791
|
}
|
|
1359
4792
|
|
|
@@ -1441,9 +4874,131 @@ function validate(task, context = {}) {
|
|
|
1441
4874
|
};
|
|
1442
4875
|
}
|
|
1443
4876
|
|
|
4877
|
+
// src/workspace.ts
|
|
4878
|
+
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
4879
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
4880
|
+
var COOP_DIR_NAME = ".coop";
|
|
4881
|
+
function sanitizeProjectId(value, fallback) {
|
|
4882
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
4883
|
+
return normalized || fallback;
|
|
4884
|
+
}
|
|
4885
|
+
function coop_workspace_dir(repoRoot) {
|
|
4886
|
+
return import_node_path9.default.join(import_node_path9.default.resolve(repoRoot), COOP_DIR_NAME);
|
|
4887
|
+
}
|
|
4888
|
+
function coop_projects_dir(repoRoot) {
|
|
4889
|
+
return import_node_path9.default.join(coop_workspace_dir(repoRoot), "projects");
|
|
4890
|
+
}
|
|
4891
|
+
function coop_workspace_config_path(repoRoot) {
|
|
4892
|
+
return import_node_path9.default.join(coop_workspace_dir(repoRoot), "config.yml");
|
|
4893
|
+
}
|
|
4894
|
+
function coop_project_root(repoRoot, projectId) {
|
|
4895
|
+
return import_node_path9.default.join(coop_projects_dir(repoRoot), projectId);
|
|
4896
|
+
}
|
|
4897
|
+
function coop_project_config_path(projectRoot) {
|
|
4898
|
+
return import_node_path9.default.join(projectRoot, "config.yml");
|
|
4899
|
+
}
|
|
4900
|
+
function repo_default_project_id(repoRoot) {
|
|
4901
|
+
return sanitizeProjectId(import_node_path9.default.basename(import_node_path9.default.resolve(repoRoot)), "workspace");
|
|
4902
|
+
}
|
|
4903
|
+
function repo_default_project_name(repoRoot) {
|
|
4904
|
+
const base = import_node_path9.default.basename(import_node_path9.default.resolve(repoRoot)).trim();
|
|
4905
|
+
return base || "COOP Workspace";
|
|
4906
|
+
}
|
|
4907
|
+
function has_v2_projects_layout(repoRoot) {
|
|
4908
|
+
return import_node_fs14.default.existsSync(coop_projects_dir(repoRoot));
|
|
4909
|
+
}
|
|
4910
|
+
function has_legacy_project_layout(repoRoot) {
|
|
4911
|
+
const workspaceDir = coop_workspace_dir(repoRoot);
|
|
4912
|
+
return import_node_fs14.default.existsSync(workspaceDir) && import_node_fs14.default.existsSync(import_node_path9.default.join(workspaceDir, "config.yml")) && !import_node_fs14.default.existsSync(coop_projects_dir(repoRoot));
|
|
4913
|
+
}
|
|
4914
|
+
function read_workspace_config(repoRoot) {
|
|
4915
|
+
const configPath = coop_workspace_config_path(repoRoot);
|
|
4916
|
+
if (!import_node_fs14.default.existsSync(configPath) || has_legacy_project_layout(repoRoot)) {
|
|
4917
|
+
return { version: 2 };
|
|
4918
|
+
}
|
|
4919
|
+
return parseYamlFile(configPath);
|
|
4920
|
+
}
|
|
4921
|
+
function write_workspace_config(repoRoot, config) {
|
|
4922
|
+
import_node_fs14.default.mkdirSync(coop_workspace_dir(repoRoot), { recursive: true });
|
|
4923
|
+
writeYamlFile(coop_workspace_config_path(repoRoot), {
|
|
4924
|
+
version: config.version ?? 2,
|
|
4925
|
+
...config.current_project ? { current_project: config.current_project } : {}
|
|
4926
|
+
});
|
|
4927
|
+
}
|
|
4928
|
+
function read_project_config(projectRoot) {
|
|
4929
|
+
return parseYamlFile(coop_project_config_path(projectRoot));
|
|
4930
|
+
}
|
|
4931
|
+
function project_ref_from_config(repoRoot, projectRoot, layout) {
|
|
4932
|
+
const config = read_project_config(projectRoot);
|
|
4933
|
+
const repoName = repo_default_project_name(repoRoot);
|
|
4934
|
+
const fallbackId = repo_default_project_id(repoRoot);
|
|
4935
|
+
return {
|
|
4936
|
+
id: sanitizeProjectId(config.project?.id ?? fallbackId, fallbackId),
|
|
4937
|
+
name: config.project?.name?.trim() || repoName,
|
|
4938
|
+
aliases: Array.isArray(config.project?.aliases) ? config.project.aliases.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [],
|
|
4939
|
+
root: projectRoot,
|
|
4940
|
+
repo_root: import_node_path9.default.resolve(repoRoot),
|
|
4941
|
+
layout
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
function list_projects(repoRoot) {
|
|
4945
|
+
if (has_v2_projects_layout(repoRoot)) {
|
|
4946
|
+
const projectsDir = coop_projects_dir(repoRoot);
|
|
4947
|
+
return import_node_fs14.default.readdirSync(projectsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => import_node_path9.default.join(projectsDir, entry.name)).filter((projectRoot) => import_node_fs14.default.existsSync(coop_project_config_path(projectRoot))).map((projectRoot) => project_ref_from_config(repoRoot, projectRoot, "v2")).sort((a, b) => a.id.localeCompare(b.id));
|
|
4948
|
+
}
|
|
4949
|
+
if (has_legacy_project_layout(repoRoot)) {
|
|
4950
|
+
return [project_ref_from_config(repoRoot, coop_workspace_dir(repoRoot), "legacy")];
|
|
4951
|
+
}
|
|
4952
|
+
return [];
|
|
4953
|
+
}
|
|
4954
|
+
function resolve_project(repoRoot, options = {}) {
|
|
4955
|
+
const projects = list_projects(repoRoot);
|
|
4956
|
+
const requested = options.project?.trim().toLowerCase();
|
|
4957
|
+
if (requested) {
|
|
4958
|
+
const match = projects.find(
|
|
4959
|
+
(project) => project.id.toLowerCase() === requested || project.name.toLowerCase() === requested || project.aliases.some((alias) => alias.toLowerCase() === requested)
|
|
4960
|
+
);
|
|
4961
|
+
if (!match) {
|
|
4962
|
+
throw new Error(`Project '${options.project}' not found.`);
|
|
4963
|
+
}
|
|
4964
|
+
return match;
|
|
4965
|
+
}
|
|
4966
|
+
if (projects.length === 1) {
|
|
4967
|
+
return projects[0];
|
|
4968
|
+
}
|
|
4969
|
+
const workspaceConfig = read_workspace_config(repoRoot);
|
|
4970
|
+
if (workspaceConfig.current_project) {
|
|
4971
|
+
const match = projects.find((project) => project.id === workspaceConfig.current_project);
|
|
4972
|
+
if (match) return match;
|
|
4973
|
+
}
|
|
4974
|
+
if (!options.require && projects.length === 0) {
|
|
4975
|
+
return {
|
|
4976
|
+
id: repo_default_project_id(repoRoot),
|
|
4977
|
+
name: repo_default_project_name(repoRoot),
|
|
4978
|
+
aliases: [],
|
|
4979
|
+
root: coop_project_root(repoRoot, repo_default_project_id(repoRoot)),
|
|
4980
|
+
repo_root: import_node_path9.default.resolve(repoRoot),
|
|
4981
|
+
layout: "v2"
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
if (projects.length === 0) {
|
|
4985
|
+
throw new Error("No COOP project found. Run 'coop init'.");
|
|
4986
|
+
}
|
|
4987
|
+
throw new Error("Multiple COOP projects found. Pass --project <id> or run 'coop project use <id>'.");
|
|
4988
|
+
}
|
|
4989
|
+
function ensure_workspace_layout(repoRoot) {
|
|
4990
|
+
const workspaceDir = coop_workspace_dir(repoRoot);
|
|
4991
|
+
import_node_fs14.default.mkdirSync(workspaceDir, { recursive: true });
|
|
4992
|
+
import_node_fs14.default.mkdirSync(coop_projects_dir(repoRoot), { recursive: true });
|
|
4993
|
+
return workspaceDir;
|
|
4994
|
+
}
|
|
4995
|
+
function is_project_initialized(projectRoot) {
|
|
4996
|
+
return import_node_fs14.default.existsSync(coop_project_config_path(projectRoot));
|
|
4997
|
+
}
|
|
4998
|
+
|
|
1444
4999
|
// src/core.ts
|
|
1445
|
-
var
|
|
1446
|
-
var
|
|
5000
|
+
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
5001
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1447
5002
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
1448
5003
|
|
|
1449
5004
|
// src/types.ts
|
|
@@ -1460,7 +5015,7 @@ var ITEM_DIRS = {
|
|
|
1460
5015
|
spike: "spikes"
|
|
1461
5016
|
};
|
|
1462
5017
|
var COOP_DIR = ".coop";
|
|
1463
|
-
var
|
|
5018
|
+
var DEFAULT_CONFIG3 = {
|
|
1464
5019
|
spec_version: 1,
|
|
1465
5020
|
id_prefix: "COOP",
|
|
1466
5021
|
id_strategy: "text"
|
|
@@ -1469,17 +5024,17 @@ function toIdKey(value) {
|
|
|
1469
5024
|
return value.trim().toUpperCase();
|
|
1470
5025
|
}
|
|
1471
5026
|
function repoRootByPackage(cwd) {
|
|
1472
|
-
let current =
|
|
5027
|
+
let current = import_node_path10.default.resolve(cwd);
|
|
1473
5028
|
let lastWorkspaceRoot = null;
|
|
1474
5029
|
while (true) {
|
|
1475
|
-
const packageJson =
|
|
1476
|
-
const workspaceYaml =
|
|
1477
|
-
if (
|
|
5030
|
+
const packageJson = import_node_path10.default.join(current, "package.json");
|
|
5031
|
+
const workspaceYaml = import_node_path10.default.join(current, "pnpm-workspace.yaml");
|
|
5032
|
+
if (import_node_fs15.default.existsSync(packageJson) && import_node_fs15.default.existsSync(workspaceYaml)) {
|
|
1478
5033
|
lastWorkspaceRoot = current;
|
|
1479
|
-
const hasCoop =
|
|
5034
|
+
const hasCoop = import_node_fs15.default.existsSync(import_node_path10.default.join(current, COOP_DIR, "config.yml"));
|
|
1480
5035
|
if (hasCoop) return current;
|
|
1481
5036
|
}
|
|
1482
|
-
const parent =
|
|
5037
|
+
const parent = import_node_path10.default.dirname(current);
|
|
1483
5038
|
if (parent === current) return lastWorkspaceRoot;
|
|
1484
5039
|
current = parent;
|
|
1485
5040
|
}
|
|
@@ -1488,24 +5043,24 @@ function findRepoRoot(cwd = process.cwd()) {
|
|
|
1488
5043
|
return repoRootByPackage(cwd);
|
|
1489
5044
|
}
|
|
1490
5045
|
function configPathFor(rootDir, workspaceDir) {
|
|
1491
|
-
return
|
|
5046
|
+
return import_node_path10.default.join(rootDir, workspaceDir, "config.yml");
|
|
1492
5047
|
}
|
|
1493
5048
|
function backlogPathFor(rootDir, workspaceDir) {
|
|
1494
|
-
return
|
|
5049
|
+
return import_node_path10.default.join(rootDir, workspaceDir, "backlog");
|
|
1495
5050
|
}
|
|
1496
5051
|
function releasesPathFor(rootDir, workspaceDir) {
|
|
1497
|
-
return
|
|
5052
|
+
return import_node_path10.default.join(rootDir, workspaceDir, "releases");
|
|
1498
5053
|
}
|
|
1499
5054
|
function detectWorkspaceDir(rootDir) {
|
|
1500
|
-
if (
|
|
1501
|
-
if (
|
|
5055
|
+
if (import_node_fs15.default.existsSync(configPathFor(rootDir, COOP_DIR))) return COOP_DIR;
|
|
5056
|
+
if (import_node_fs15.default.existsSync(import_node_path10.default.join(rootDir, COOP_DIR))) return COOP_DIR;
|
|
1502
5057
|
return null;
|
|
1503
5058
|
}
|
|
1504
5059
|
function preferredWorkspaceDir(rootDir) {
|
|
1505
5060
|
return detectWorkspaceDir(rootDir) ?? COOP_DIR;
|
|
1506
5061
|
}
|
|
1507
5062
|
function missingConfigError(rootDir) {
|
|
1508
|
-
const coopConfig =
|
|
5063
|
+
const coopConfig = import_node_path10.default.relative(rootDir, configPathFor(rootDir, COOP_DIR));
|
|
1509
5064
|
return new Error(`COOP config missing at ${coopConfig}. Run: coop init`);
|
|
1510
5065
|
}
|
|
1511
5066
|
function parseConfig(raw) {
|
|
@@ -1516,9 +5071,9 @@ function parseConfig(raw) {
|
|
|
1516
5071
|
if (ix <= 0) continue;
|
|
1517
5072
|
map.set(line.slice(0, ix).trim(), line.slice(ix + 1).trim());
|
|
1518
5073
|
}
|
|
1519
|
-
const spec = Number(map.get("spec_version") ??
|
|
1520
|
-
const idPrefix = map.get("id_prefix") ??
|
|
1521
|
-
const strategy = String(map.get("id_strategy") ??
|
|
5074
|
+
const spec = Number(map.get("spec_version") ?? DEFAULT_CONFIG3.spec_version);
|
|
5075
|
+
const idPrefix = map.get("id_prefix") ?? DEFAULT_CONFIG3.id_prefix;
|
|
5076
|
+
const strategy = String(map.get("id_strategy") ?? DEFAULT_CONFIG3.id_strategy ?? "text").trim().toLowerCase();
|
|
1522
5077
|
const rawNextId = map.get("next_id");
|
|
1523
5078
|
const nextId = rawNextId != null ? Number(rawNextId) : void 0;
|
|
1524
5079
|
if (!Number.isInteger(spec) || spec <= 0) throw new Error("config.yml must define a numeric spec_version.");
|
|
@@ -1551,10 +5106,10 @@ function configToString(config) {
|
|
|
1551
5106
|
return lines.join("\n");
|
|
1552
5107
|
}
|
|
1553
5108
|
function toPortablePath(value) {
|
|
1554
|
-
return value.split(
|
|
5109
|
+
return value.split(import_node_path10.default.sep).join("/");
|
|
1555
5110
|
}
|
|
1556
5111
|
function ensureReleasesDir(rootDir, workspaceDir) {
|
|
1557
|
-
|
|
5112
|
+
import_node_fs15.default.mkdirSync(releasesPathFor(rootDir, workspaceDir), { recursive: true });
|
|
1558
5113
|
}
|
|
1559
5114
|
function releaseHeader(date) {
|
|
1560
5115
|
return `## ${date}`;
|
|
@@ -1576,12 +5131,12 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
|
|
|
1576
5131
|
ensureReleasesDir(rootDir, workspaceDir);
|
|
1577
5132
|
const now = /* @__PURE__ */ new Date();
|
|
1578
5133
|
const date = now.toISOString().slice(0, 10);
|
|
1579
|
-
const releasePath =
|
|
5134
|
+
const releasePath = import_node_path10.default.join(releasesPathFor(rootDir, workspaceDir), `${date}.md`);
|
|
1580
5135
|
const heading = "# COOP Release Notes";
|
|
1581
5136
|
const dayHeader = releaseHeader(date);
|
|
1582
5137
|
const entry = releaseEntryLine(item, previousStatus, nextStatus);
|
|
1583
|
-
if (!
|
|
1584
|
-
|
|
5138
|
+
if (!import_node_fs15.default.existsSync(releasePath)) {
|
|
5139
|
+
import_node_fs15.default.writeFileSync(
|
|
1585
5140
|
releasePath,
|
|
1586
5141
|
[
|
|
1587
5142
|
`${heading}
|
|
@@ -1594,10 +5149,10 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
|
|
|
1594
5149
|
].join("\n"),
|
|
1595
5150
|
"utf8"
|
|
1596
5151
|
);
|
|
1597
|
-
return toPortablePath(
|
|
5152
|
+
return toPortablePath(import_node_path10.default.relative(rootDir, releasePath));
|
|
1598
5153
|
}
|
|
1599
|
-
const existing =
|
|
1600
|
-
if (hasReleaseEntry(existing, item.id)) return toPortablePath(
|
|
5154
|
+
const existing = import_node_fs15.default.readFileSync(releasePath, "utf8");
|
|
5155
|
+
if (hasReleaseEntry(existing, item.id)) return toPortablePath(import_node_path10.default.relative(rootDir, releasePath));
|
|
1601
5156
|
let nextContent = existing;
|
|
1602
5157
|
if (!existing.includes(`## ${date}`)) {
|
|
1603
5158
|
if (!nextContent.endsWith("\n")) nextContent += "\n";
|
|
@@ -1607,9 +5162,9 @@ function appendReleaseEntry(rootDir, workspaceDir, item, previousStatus, nextSta
|
|
|
1607
5162
|
if (!nextContent.endsWith("\n")) nextContent += "\n";
|
|
1608
5163
|
nextContent += `${entry}
|
|
1609
5164
|
`;
|
|
1610
|
-
|
|
5165
|
+
import_node_fs15.default.writeFileSync(releasePath, `${nextContent}
|
|
1611
5166
|
`, "utf8");
|
|
1612
|
-
return toPortablePath(
|
|
5167
|
+
return toPortablePath(import_node_path10.default.relative(rootDir, releasePath));
|
|
1613
5168
|
}
|
|
1614
5169
|
function completeItem(rootDir, id) {
|
|
1615
5170
|
const state = loadState(rootDir);
|
|
@@ -1687,28 +5242,28 @@ function validateAndNormalize(data, sourceFile) {
|
|
|
1687
5242
|
};
|
|
1688
5243
|
}
|
|
1689
5244
|
function parseItem(filePath, rootDir) {
|
|
1690
|
-
const raw =
|
|
5245
|
+
const raw = import_node_fs15.default.readFileSync(filePath, "utf8");
|
|
1691
5246
|
const parsed = (0, import_gray_matter.default)(raw);
|
|
1692
|
-
const data = validateAndNormalize(parsed.data,
|
|
5247
|
+
const data = validateAndNormalize(parsed.data, import_node_path10.default.relative(rootDir, filePath));
|
|
1693
5248
|
return {
|
|
1694
5249
|
...data,
|
|
1695
5250
|
body: parsed.content || "",
|
|
1696
|
-
filePath:
|
|
5251
|
+
filePath: import_node_path10.default.relative(rootDir, filePath)
|
|
1697
5252
|
};
|
|
1698
5253
|
}
|
|
1699
5254
|
function walk(dir) {
|
|
1700
5255
|
const out = [];
|
|
1701
|
-
if (!
|
|
1702
|
-
const entries =
|
|
5256
|
+
if (!import_node_fs15.default.existsSync(dir)) return out;
|
|
5257
|
+
const entries = import_node_fs15.default.readdirSync(dir, { withFileTypes: true });
|
|
1703
5258
|
for (const entry of entries) {
|
|
1704
|
-
const file =
|
|
5259
|
+
const file = import_node_path10.default.join(dir, entry.name);
|
|
1705
5260
|
if (entry.isDirectory()) out.push(...walk(file));
|
|
1706
5261
|
if (entry.isFile() && file.endsWith(".md")) out.push(file);
|
|
1707
5262
|
}
|
|
1708
5263
|
return out;
|
|
1709
5264
|
}
|
|
1710
5265
|
function itemPath(type, id, rootDir, workspaceDir) {
|
|
1711
|
-
return
|
|
5266
|
+
return import_node_path10.default.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type], `${id}.md`);
|
|
1712
5267
|
}
|
|
1713
5268
|
function normalizeFrontmatterValue(value) {
|
|
1714
5269
|
if (value == null) return void 0;
|
|
@@ -1795,30 +5350,30 @@ function nextGeneratedId(config, title, existing) {
|
|
|
1795
5350
|
}
|
|
1796
5351
|
function ensureCoopLayout(rootDir) {
|
|
1797
5352
|
const workspaceDir = preferredWorkspaceDir(rootDir);
|
|
1798
|
-
const root =
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
5353
|
+
const root = import_node_path10.default.join(rootDir, workspaceDir);
|
|
5354
|
+
import_node_fs15.default.mkdirSync(root, { recursive: true });
|
|
5355
|
+
import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "releases"), { recursive: true });
|
|
5356
|
+
import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "plans"), { recursive: true });
|
|
5357
|
+
import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "views"), { recursive: true });
|
|
5358
|
+
import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "templates"), { recursive: true });
|
|
1804
5359
|
for (const dir of Object.values(ITEM_DIRS)) {
|
|
1805
|
-
|
|
5360
|
+
import_node_fs15.default.mkdirSync(import_node_path10.default.join(root, "backlog", dir), { recursive: true });
|
|
1806
5361
|
}
|
|
1807
|
-
const configFile =
|
|
1808
|
-
if (!
|
|
1809
|
-
|
|
5362
|
+
const configFile = import_node_path10.default.join(root, "config.yml");
|
|
5363
|
+
if (!import_node_fs15.default.existsSync(configFile)) {
|
|
5364
|
+
import_node_fs15.default.writeFileSync(configFile, configToString(DEFAULT_CONFIG3), "utf8");
|
|
1810
5365
|
}
|
|
1811
5366
|
}
|
|
1812
5367
|
function loadState(rootDir) {
|
|
1813
5368
|
const workspaceDir = detectWorkspaceDir(rootDir);
|
|
1814
5369
|
if (!workspaceDir) throw missingConfigError(rootDir);
|
|
1815
5370
|
const configPath = configPathFor(rootDir, workspaceDir);
|
|
1816
|
-
if (!
|
|
1817
|
-
const config = parseConfig(
|
|
5371
|
+
if (!import_node_fs15.default.existsSync(configPath)) throw missingConfigError(rootDir);
|
|
5372
|
+
const config = parseConfig(import_node_fs15.default.readFileSync(configPath, "utf8"));
|
|
1818
5373
|
const items = [];
|
|
1819
5374
|
const itemsById = /* @__PURE__ */ new Map();
|
|
1820
5375
|
for (const type of ITEM_TYPES) {
|
|
1821
|
-
const dir =
|
|
5376
|
+
const dir = import_node_path10.default.join(backlogPathFor(rootDir, workspaceDir), ITEM_DIRS[type]);
|
|
1822
5377
|
const files = walk(dir);
|
|
1823
5378
|
for (const file of files) {
|
|
1824
5379
|
const item = parseItem(file, rootDir);
|
|
@@ -1902,21 +5457,21 @@ function createItem(rootDir, params) {
|
|
|
1902
5457
|
parent_id: params.parent_id
|
|
1903
5458
|
};
|
|
1904
5459
|
const itemPathName = itemPath(params.type, item.id, rootDir, state.workspaceDir);
|
|
1905
|
-
|
|
5460
|
+
import_node_fs15.default.writeFileSync(itemPathName, serialize(item, params.body || ""), "utf8");
|
|
1906
5461
|
if ((config.id_strategy ?? "text") === "counter") {
|
|
1907
5462
|
const numericMatch = /-(\d+)$/.exec(item.id);
|
|
1908
5463
|
if (numericMatch) {
|
|
1909
5464
|
const numericValue = Number(numericMatch[1]);
|
|
1910
5465
|
if (Number.isInteger(numericValue) && numericValue >= (config.next_id ?? 1)) {
|
|
1911
5466
|
config.next_id = numericValue + 1;
|
|
1912
|
-
|
|
5467
|
+
import_node_fs15.default.writeFileSync(configPathFor(rootDir, state.workspaceDir), configToString(config), "utf8");
|
|
1913
5468
|
}
|
|
1914
5469
|
}
|
|
1915
5470
|
}
|
|
1916
5471
|
return {
|
|
1917
5472
|
...item,
|
|
1918
5473
|
body: params.body || "",
|
|
1919
|
-
filePath:
|
|
5474
|
+
filePath: import_node_path10.default.relative(rootDir, itemPathName)
|
|
1920
5475
|
};
|
|
1921
5476
|
}
|
|
1922
5477
|
function updateItem(rootDir, id, patch) {
|
|
@@ -1942,8 +5497,8 @@ function updateItem(rootDir, id, patch) {
|
|
|
1942
5497
|
};
|
|
1943
5498
|
if (!ITEM_TYPES.includes(next.type)) throw new Error(`Unknown type ${next.type}.`);
|
|
1944
5499
|
if (!ITEM_STATUSES.includes(next.status)) throw new Error(`Unknown status ${next.status}.`);
|
|
1945
|
-
const filePath =
|
|
1946
|
-
|
|
5500
|
+
const filePath = import_node_path10.default.join(rootDir, existing.filePath);
|
|
5501
|
+
import_node_fs15.default.writeFileSync(filePath, serialize(next, patch.body || existing.body), "utf8");
|
|
1947
5502
|
return {
|
|
1948
5503
|
...next,
|
|
1949
5504
|
body: patch.body || existing.body,
|
|
@@ -1959,7 +5514,7 @@ function deleteItem(rootDir, id) {
|
|
|
1959
5514
|
if (children.length > 0) {
|
|
1960
5515
|
throw new Error(`Cannot delete ${existing.id} because it has ${children.length} child item(s). Remove children first.`);
|
|
1961
5516
|
}
|
|
1962
|
-
|
|
5517
|
+
import_node_fs15.default.unlinkSync(import_node_path10.default.join(rootDir, existing.filePath));
|
|
1963
5518
|
return existing;
|
|
1964
5519
|
}
|
|
1965
5520
|
function renderAgentPrompt(item) {
|
|
@@ -1998,8 +5553,8 @@ function validateRepo(rootDir) {
|
|
|
1998
5553
|
const errors = [];
|
|
1999
5554
|
const warnings = [];
|
|
2000
5555
|
const workspaceDir = detectWorkspaceDir(rootDir);
|
|
2001
|
-
if (!workspaceDir || !
|
|
2002
|
-
errors.push("Missing
|
|
5556
|
+
if (!workspaceDir || !import_node_fs15.default.existsSync(configPathFor(rootDir, workspaceDir))) {
|
|
5557
|
+
errors.push("Missing COOP config. Run coop init first.");
|
|
2003
5558
|
return { valid: false, errors, warnings };
|
|
2004
5559
|
}
|
|
2005
5560
|
let state;
|
|
@@ -2032,13 +5587,20 @@ function validateRepo(rootDir) {
|
|
|
2032
5587
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2033
5588
|
0 && (module.exports = {
|
|
2034
5589
|
ArtifactType,
|
|
5590
|
+
COOP_EVENT_TYPES,
|
|
2035
5591
|
CURRENT_SCHEMA_VERSION,
|
|
5592
|
+
CoopEventEmitter,
|
|
5593
|
+
DEFAULT_SCORE_WEIGHTS,
|
|
2036
5594
|
DeliveryStatus,
|
|
2037
5595
|
ExecutorType,
|
|
2038
5596
|
ITEM_STATUSES,
|
|
2039
5597
|
ITEM_TYPES,
|
|
2040
5598
|
IdeaStatus,
|
|
5599
|
+
IndexManager,
|
|
5600
|
+
MIGRATIONS,
|
|
2041
5601
|
RiskLevel,
|
|
5602
|
+
RunStatus,
|
|
5603
|
+
RunStepStatus,
|
|
2042
5604
|
RunbookAction,
|
|
2043
5605
|
TaskComplexity,
|
|
2044
5606
|
TaskDeterminism,
|
|
@@ -2047,23 +5609,63 @@ function validateRepo(rootDir) {
|
|
|
2047
5609
|
TaskType,
|
|
2048
5610
|
VALID_TASK_TRANSITIONS,
|
|
2049
5611
|
VALID_TRANSITIONS,
|
|
5612
|
+
allocate,
|
|
5613
|
+
allocate_ai,
|
|
5614
|
+
allocate_ai_tokens,
|
|
5615
|
+
analyze_feasibility,
|
|
5616
|
+
analyze_what_if,
|
|
5617
|
+
build_capacity_ledger,
|
|
2050
5618
|
build_graph,
|
|
2051
5619
|
check_blocked,
|
|
5620
|
+
check_permission,
|
|
2052
5621
|
check_unblocked,
|
|
5622
|
+
check_wip,
|
|
2053
5623
|
completeItem,
|
|
5624
|
+
complexity_penalty,
|
|
2054
5625
|
compute_all_readiness,
|
|
5626
|
+
compute_critical_path,
|
|
2055
5627
|
compute_readiness,
|
|
2056
5628
|
compute_readiness_with_corrections,
|
|
5629
|
+
compute_score,
|
|
5630
|
+
compute_velocity,
|
|
5631
|
+
coop_project_config_path,
|
|
5632
|
+
coop_project_root,
|
|
5633
|
+
coop_projects_dir,
|
|
5634
|
+
coop_workspace_config_path,
|
|
5635
|
+
coop_workspace_dir,
|
|
2057
5636
|
createItem,
|
|
5637
|
+
create_seeded_rng,
|
|
5638
|
+
critical_path_weight,
|
|
2058
5639
|
deleteItem,
|
|
5640
|
+
dependency_unlock_weight,
|
|
2059
5641
|
detect_cycle,
|
|
5642
|
+
detect_delivery_risks,
|
|
5643
|
+
determinism_weight,
|
|
5644
|
+
effective_weekly_hours,
|
|
5645
|
+
effort_or_default,
|
|
2060
5646
|
ensureCoopLayout,
|
|
5647
|
+
ensure_workspace_layout,
|
|
5648
|
+
executor_fit_weight,
|
|
5649
|
+
external_dependencies_for_task,
|
|
2061
5650
|
extract_subgraph,
|
|
2062
5651
|
findRepoRoot,
|
|
2063
5652
|
find_external_dependencies,
|
|
2064
5653
|
getItemById,
|
|
5654
|
+
get_remaining_tokens,
|
|
5655
|
+
get_user_role,
|
|
5656
|
+
has_legacy_project_layout,
|
|
5657
|
+
has_v2_projects_layout,
|
|
5658
|
+
is_external_dependency,
|
|
5659
|
+
is_project_initialized,
|
|
5660
|
+
list_projects,
|
|
2065
5661
|
loadState,
|
|
5662
|
+
load_auth_config,
|
|
5663
|
+
load_completed_runs,
|
|
2066
5664
|
load_graph,
|
|
5665
|
+
load_plugins,
|
|
5666
|
+
migrate_repository,
|
|
5667
|
+
migrate_task,
|
|
5668
|
+
monte_carlo_forecast,
|
|
2067
5669
|
parseDeliveryContent,
|
|
2068
5670
|
parseDeliveryFile,
|
|
2069
5671
|
parseFrontmatterContent,
|
|
@@ -2074,16 +5676,38 @@ function validateRepo(rootDir) {
|
|
|
2074
5676
|
parseTaskFile,
|
|
2075
5677
|
parseYamlContent,
|
|
2076
5678
|
parseYamlFile,
|
|
5679
|
+
parse_external_dependency,
|
|
2077
5680
|
partition_by_readiness,
|
|
5681
|
+
pert_hours,
|
|
5682
|
+
pert_stddev,
|
|
5683
|
+
priority_weight,
|
|
2078
5684
|
queryItems,
|
|
5685
|
+
read_project_config,
|
|
2079
5686
|
read_schema_version,
|
|
5687
|
+
read_workspace_config,
|
|
2080
5688
|
renderAgentPrompt,
|
|
5689
|
+
repo_default_project_id,
|
|
5690
|
+
repo_default_project_name,
|
|
5691
|
+
resolve_external_dependencies,
|
|
5692
|
+
resolve_project,
|
|
5693
|
+
risk_penalty,
|
|
5694
|
+
run_hook,
|
|
5695
|
+
run_monte_carlo_chunk,
|
|
5696
|
+
run_plugins_for_event,
|
|
5697
|
+
sample_pert_beta,
|
|
5698
|
+
sample_task_hours,
|
|
5699
|
+
schedule_next,
|
|
5700
|
+
simulate_schedule,
|
|
2081
5701
|
stringifyFrontmatter,
|
|
5702
|
+
stringifyYamlContent,
|
|
5703
|
+
task_effort_hours,
|
|
2082
5704
|
topological_sort,
|
|
2083
5705
|
transition,
|
|
2084
5706
|
transitive_dependencies,
|
|
2085
5707
|
transitive_dependents,
|
|
5708
|
+
type_weight,
|
|
2086
5709
|
updateItem,
|
|
5710
|
+
urgency_weight,
|
|
2087
5711
|
validate,
|
|
2088
5712
|
validateReferential,
|
|
2089
5713
|
validateRepo,
|
|
@@ -2093,5 +5717,7 @@ function validateRepo(rootDir) {
|
|
|
2093
5717
|
validate_graph,
|
|
2094
5718
|
validate_transition,
|
|
2095
5719
|
writeTask,
|
|
2096
|
-
|
|
5720
|
+
writeYamlFile,
|
|
5721
|
+
write_schema_version,
|
|
5722
|
+
write_workspace_config
|
|
2097
5723
|
});
|