yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project_plan.cc
Go to the documentation of this file.
1#include "core/project_plan.h"
2
3#include <algorithm>
4#include <cstdlib>
5#include <filesystem>
6#include <fstream>
7#include <initializer_list>
8#include <optional>
9#include <set>
10#include <sstream>
11#include <system_error>
12#include <utility>
13
14#include "absl/strings/str_format.h"
15#include "core/project.h"
18#include "util/macro.h"
19
20namespace yaze::core {
21namespace {
22
23absl::StatusOr<std::string> ReadSource(const std::string& path) {
24 std::error_code error;
25 const auto status = std::filesystem::status(path, error);
26 if (error == std::errc::no_such_file_or_directory ||
27 (!error && !std::filesystem::exists(status))) {
28 return absl::NotFoundError(
29 absl::StrFormat("File does not exist: %s", path));
30 }
31 if (error || !std::filesystem::is_regular_file(status)) {
32 return absl::FailedPreconditionError(
33 absl::StrFormat("Not a readable regular file: %s", path));
34 }
35 std::ifstream file(path, std::ios::binary);
36 if (!file.is_open()) {
37 std::error_code error;
38 if (!std::filesystem::exists(path, error) && !error) {
39 return absl::NotFoundError(
40 absl::StrFormat("File does not exist: %s", path));
41 }
42 return absl::PermissionDeniedError(
43 absl::StrFormat("Cannot read file: %s", path));
44 }
45 std::stringstream buffer;
46 buffer << file.rdbuf();
47 if (file.bad()) {
48 return absl::DataLossError(
49 absl::StrFormat("Failed to read file: %s", path));
50 }
51 return buffer.str();
52}
53
54template <typename Entry>
55absl::Status ValidateIds(const std::vector<Entry>& entries, const char* kind) {
56 // AppleClang 17 rejects absl's SourceLocation default argument when the
57 // error is built inside a range-for in a template, so build it after.
58 std::set<std::string> ids;
59 const Entry* invalid = nullptr;
60 for (const auto& entry : entries) {
61 if (entry.id.empty() || !ids.insert(entry.id).second) {
62 invalid = &entry;
63 break;
64 }
65 }
66 if (invalid == nullptr) {
67 return absl::OkStatus();
68 }
69 return absl::InvalidArgumentError(absl::StrFormat(
70 "%s IDs must be nonempty and unique: %s", kind, invalid->id));
71}
72
73template <typename Entry>
74std::set<std::string> EntryIds(const std::vector<Entry>& entries) {
75 std::set<std::string> ids;
76 for (const auto& entry : entries) {
77 ids.insert(entry.id);
78 }
79 return ids;
80}
81
82std::string UnusedId(const char* prefix, const std::set<std::string>& ids) {
83 for (size_t number = 1; number <= ids.size() + 1; ++number) {
84 const std::string id = absl::StrFormat("%s%zu", prefix, number);
85 if (!ids.contains(id)) {
86 return id;
87 }
88 }
89 return "";
90}
91
92void RewriteTargets(std::vector<std::string>* targets,
93 const std::string& before, const std::string& after) {
94 for (auto& target : *targets) {
95 if (target == before) {
96 target = after;
97 }
98 }
99}
100
101class Reader {
102 public:
103 explicit Reader(std::string context) : context_(std::move(context)) {}
104
105 absl::Status status() const { return status_; }
106
107 void String(const PlanJson& obj, const char* key, std::string* out) {
108 auto it = obj.find(key);
109 if (it == obj.end() || it->is_null()) {
110 return;
111 }
112 if (!it->is_string()) {
113 Fail(key, "a string");
114 return;
115 }
116 *out = it->get<std::string>();
117 }
118
119 void Bool(const PlanJson& obj, const char* key, bool* out) {
120 auto it = obj.find(key);
121 if (it == obj.end() || it->is_null()) {
122 return;
123 }
124 if (!it->is_boolean()) {
125 Fail(key, "true or false");
126 return;
127 }
128 *out = it->get<bool>();
129 }
130
131 void Int(const PlanJson& obj, const char* key, int* out) {
132 auto it = obj.find(key);
133 if (it == obj.end() || it->is_null()) {
134 return;
135 }
136 if (!it->is_number_integer()) {
137 Fail(key, "an integer");
138 return;
139 }
140 *out = it->get<int>();
141 }
142
143 void Strings(const PlanJson& obj, const char* key,
144 std::vector<std::string>* out) {
145 auto it = obj.find(key);
146 if (it == obj.end() || it->is_null()) {
147 return;
148 }
149 if (!it->is_array()) {
150 Fail(key, "an array of strings");
151 return;
152 }
153 for (const auto& value : *it) {
154 if (!value.is_string()) {
155 Fail(key, "an array of strings");
156 return;
157 }
158 out->push_back(value.get<std::string>());
159 }
160 }
161
162 // Returns the array at `key`, or an empty array when absent.
163 const PlanJson& Array(const PlanJson& obj, const char* key) {
164 static const PlanJson kEmpty = PlanJson::array();
165 auto it = obj.find(key);
166 if (it == obj.end() || it->is_null()) {
167 return kEmpty;
168 }
169 if (!it->is_array()) {
170 Fail(key, "an array");
171 return kEmpty;
172 }
173 return *it;
174 }
175
176 bool RequireObject(const PlanJson& value, const std::string& where) {
177 if (value.is_object()) {
178 return true;
179 }
180 if (status_.ok()) {
181 status_ = absl::InvalidArgumentError(
182 absl::StrFormat("%s: %s must be an object", context_, where));
183 }
184 return false;
185 }
186
187 private:
188 void Fail(const char* key, const char* expected) {
189 if (status_.ok()) {
190 status_ = absl::InvalidArgumentError(
191 absl::StrFormat("%s: \"%s\" must be %s", context_, key, expected));
192 }
193 }
194
195 std::string context_;
196 absl::Status status_;
197};
198
199PlanJson Extras(const PlanJson& obj, std::initializer_list<const char*> known) {
200 PlanJson extra = PlanJson::object();
201 for (auto it = obj.begin(); it != obj.end(); ++it) {
202 const bool is_known =
203 std::any_of(known.begin(), known.end(),
204 [&](const char* key) { return it.key() == key; });
205 if (!is_known) {
206 extra[it.key()] = it.value();
207 }
208 }
209 return extra;
210}
211
212void PutString(PlanJson& obj, const char* key, const std::string& value) {
213 if (!value.empty()) {
214 obj[key] = value;
215 }
216}
217
218void PutStrings(PlanJson& obj, const char* key,
219 const std::vector<std::string>& values) {
220 if (!values.empty()) {
221 obj[key] = values;
222 }
223}
224
225void PutExtras(PlanJson& obj, const PlanJson& extra) {
226 if (!extra.is_object()) {
227 return;
228 }
229 for (auto it = extra.begin(); it != extra.end(); ++it) {
230 if (!obj.contains(it.key())) {
231 obj[it.key()] = it.value();
232 }
233 }
234}
235
236absl::Status ReadGoal(const PlanJson& root, PlanGoal* goal) {
237 auto it = root.find("goal");
238 if (it == root.end() || it->is_null()) {
239 return absl::OkStatus();
240 }
241 Reader reader("goal");
242 if (!reader.RequireObject(*it, "goal")) {
243 return reader.status();
244 }
245 reader.String(*it, "title", &goal->title);
246 reader.String(*it, "detail", &goal->detail);
247 reader.String(*it, "due", &goal->due);
248 goal->extra = Extras(*it, {"title", "detail", "due"});
249 return reader.status();
250}
251
252absl::Status ReadMilestones(const PlanJson& root,
253 std::vector<PlanMilestone>* out) {
254 Reader top("plan");
255 const PlanJson& list = top.Array(root, "milestones");
256 for (size_t i = 0; i < list.size() && top.status().ok(); ++i) {
257 Reader reader(absl::StrFormat("milestones[%d]", i));
258 if (!reader.RequireObject(list[i], "entry")) {
259 return reader.status();
260 }
262 reader.String(list[i], "id", &m.id);
263 reader.String(list[i], "title", &m.title);
264 reader.String(list[i], "start", &m.start);
265 reader.String(list[i], "end", &m.end);
266 reader.String(list[i], "status", &m.status);
267 reader.String(list[i], "detail", &m.detail);
268 m.extra =
269 Extras(list[i], {"id", "title", "start", "end", "status", "detail"});
270 if (!reader.status().ok()) {
271 return reader.status();
272 }
273 out->push_back(std::move(m));
274 }
275 return top.status();
276}
277
278absl::Status ReadNeeds(const PlanJson& item, const std::string& context,
279 std::vector<PlanNeed>* out) {
280 Reader top(context);
281 const PlanJson& list = top.Array(item, "needs");
282 for (size_t i = 0; i < list.size() && top.status().ok(); ++i) {
283 Reader reader(absl::StrFormat("%s.needs[%d]", context, i));
284 if (!reader.RequireObject(list[i], "entry")) {
285 return reader.status();
286 }
287 PlanNeed need;
288 reader.String(list[i], "step", &need.step);
289 reader.String(list[i], "what", &need.what);
290 need.extra = Extras(list[i], {"step", "what"});
291 if (!reader.status().ok()) {
292 return reader.status();
293 }
294 out->push_back(std::move(need));
295 }
296 return top.status();
297}
298
299absl::Status ReadRoute(const PlanJson& root, ProjectPlan* plan) {
300 auto it = root.find("route");
301 if (it == root.end() || it->is_null()) {
302 return absl::OkStatus();
303 }
304 Reader top("route");
305 if (!top.RequireObject(*it, "route")) {
306 return top.status();
307 }
308 const PlanJson& route = *it;
309 plan->route_extra = Extras(route, {"steps", "items"});
310
311 const PlanJson& steps = top.Array(route, "steps");
312 for (size_t i = 0; i < steps.size() && top.status().ok(); ++i) {
313 Reader reader(absl::StrFormat("route.steps[%d]", i));
314 if (!reader.RequireObject(steps[i], "entry")) {
315 return reader.status();
316 }
317 PlanRouteStep step;
318 reader.String(steps[i], "id", &step.id);
319 reader.String(steps[i], "code", &step.code);
320 reader.String(steps[i], "title", &step.title);
321 reader.String(steps[i], "note", &step.note);
322 reader.String(steps[i], "group", &step.group);
323 reader.String(steps[i], "kind", &step.kind);
324 reader.Strings(steps[i], "targets", &step.targets);
325 step.extra = Extras(
326 steps[i], {"id", "code", "title", "note", "group", "kind", "targets"});
327 if (!reader.status().ok()) {
328 return reader.status();
329 }
330 plan->route_steps.push_back(std::move(step));
331 }
332
333 const PlanJson& items = top.Array(route, "items");
334 for (size_t i = 0; i < items.size() && top.status().ok(); ++i) {
335 const std::string context = absl::StrFormat("route.items[%d]", i);
336 Reader reader(context);
337 if (!reader.RequireObject(items[i], "entry")) {
338 return reader.status();
339 }
340 PlanRouteItem item;
341 reader.String(items[i], "id", &item.id);
342 reader.String(items[i], "name", &item.name);
343 reader.String(items[i], "step", &item.step);
344 reader.String(items[i], "category", &item.category);
345 reader.Bool(items[i], "required", &item.required);
346 reader.String(items[i], "status", &item.status);
347 reader.String(items[i], "where", &item.where);
348 reader.String(items[i], "note", &item.note);
349 item.extra = Extras(items[i], {"id", "name", "step", "category", "required",
350 "status", "where", "note", "needs"});
351 if (!reader.status().ok()) {
352 return reader.status();
353 }
354 auto needs_status = ReadNeeds(items[i], context, &item.needs);
355 if (!needs_status.ok()) {
356 return needs_status;
357 }
358 plan->route_items.push_back(std::move(item));
359 }
360 return top.status();
361}
362
363absl::Status ReadDecisions(const PlanJson& root,
364 std::vector<PlanDecision>* out) {
365 Reader top("plan");
366 const PlanJson& list = top.Array(root, "decisions");
367 for (size_t i = 0; i < list.size() && top.status().ok(); ++i) {
368 Reader reader(absl::StrFormat("decisions[%d]", i));
369 if (!reader.RequireObject(list[i], "entry")) {
370 return reader.status();
371 }
372 PlanDecision d;
373 reader.String(list[i], "id", &d.id);
374 reader.String(list[i], "title", &d.title);
375 reader.String(list[i], "status", &d.status);
376 reader.Int(list[i], "priority", &d.priority);
377 reader.String(list[i], "recommendation", &d.recommendation);
378 reader.String(list[i], "answer", &d.answer);
379 reader.Strings(list[i], "targets", &d.targets);
380 reader.Strings(list[i], "refs", &d.refs);
381 d.extra = Extras(list[i], {"id", "title", "status", "priority",
382 "recommendation", "answer", "targets", "refs"});
383 if (!reader.status().ok()) {
384 return reader.status();
385 }
386 out->push_back(std::move(d));
387 }
388 return top.status();
389}
390
391absl::Status ReadReferences(const PlanJson& root,
392 std::vector<PlanReference>* out) {
393 Reader top("plan");
394 const PlanJson& list = top.Array(root, "references");
395 for (size_t i = 0; i < list.size() && top.status().ok(); ++i) {
396 Reader reader(absl::StrFormat("references[%d]", i));
397 if (!reader.RequireObject(list[i], "entry")) {
398 return reader.status();
399 }
401 reader.String(list[i], "label", &r.label);
402 reader.String(list[i], "path", &r.path);
403 reader.String(list[i], "author", &r.author);
404 reader.String(list[i], "date", &r.date);
405 r.extra = Extras(list[i], {"label", "path", "author", "date"});
406 if (!reader.status().ok()) {
407 return reader.status();
408 }
409 out->push_back(std::move(r));
410 }
411 return top.status();
412}
413
414} // namespace
415
416PlanTarget PlanTarget::Parse(std::string_view text) {
417 PlanTarget target;
418 const size_t colon = text.find(':');
419 if (colon == std::string_view::npos) {
420 target.id = std::string(text);
421 return target;
422 }
423 target.kind = std::string(text.substr(0, colon));
424 target.id = std::string(text.substr(colon + 1));
425 return target;
426}
427
429 if ((kind != "room" && kind != "map") || id.empty()) {
430 return -1;
431 }
432 char* end = nullptr;
433 const long value = std::strtol(id.c_str(), &end, 0);
434 if (end == id.c_str() || *end != '\0' || value < 0) {
435 return -1;
436 }
437 return static_cast<int>(value);
438}
439
441 ProjectPlan plan;
442 plan.title = std::move(title);
443 return plan;
444}
445
446absl::StatusOr<ProjectPlan> ProjectPlan::Parse(std::string_view text) {
447 PlanJson root = PlanJson::parse(text.begin(), text.end(), nullptr,
448 /*allow_exceptions=*/false);
449 if (root.is_discarded()) {
450 return absl::InvalidArgumentError("Plan file is not valid JSON");
451 }
452 if (!root.is_object()) {
453 return absl::InvalidArgumentError("Plan file must be a JSON object");
454 }
455 auto schema = root.find("schema");
456 if (schema == root.end() || !schema->is_string() ||
457 schema->get<std::string>() != kProjectPlanSchema) {
458 return absl::InvalidArgumentError(
459 absl::StrFormat("Plan file must declare \"schema\": \"%s\"",
460 std::string(kProjectPlanSchema)));
461 }
462
463 ProjectPlan plan;
464 Reader reader("plan");
465 reader.String(root, "title", &plan.title);
466 reader.String(root, "about", &plan.about);
467 if (!reader.status().ok()) {
468 return reader.status();
469 }
470 for (auto status :
471 {ReadGoal(root, &plan.goal), ReadMilestones(root, &plan.milestones),
472 ReadRoute(root, &plan), ReadDecisions(root, &plan.decisions),
473 ReadReferences(root, &plan.references)}) {
474 if (!status.ok()) {
475 return status;
476 }
477 }
478 plan.extra = Extras(root, {"schema", "title", "about", "goal", "milestones",
479 "route", "decisions", "references"});
481 return plan;
482}
483
484absl::StatusOr<ProjectPlan> ProjectPlan::LoadFromFile(const std::string& path,
485 std::string* source) {
486 ASSIGN_OR_RETURN(auto contents, ReadSource(path));
487 auto plan = Parse(contents);
488 if (!plan.ok()) {
489 return absl::Status(
490 plan.status().code(),
491 absl::StrFormat("%s: %s", path, plan.status().message()));
492 }
493 if (source != nullptr) {
494 *source = std::move(contents);
495 }
496 return plan;
497}
498
500 const std::string& path) {
501 if (project == nullptr || project->filepath.empty() || path.empty()) {
502 return absl::InvalidArgumentError(
503 "A project file and new plan path are required");
504 }
505 const auto plan = CreateEmpty(project->name);
506#ifdef __EMSCRIPTEN__
507 RETURN_IF_ERROR(plan.SaveToFile(path));
508 auto updated = *project;
509 updated.planning.file = updated.GetRelativePath(path);
510 updated.planning.section_present = true;
511 const auto status = updated.Save();
512 if (!status.ok()) {
513 const auto current = ReadSource(path);
514 if (!current.ok() || *current != plan.Serialize()) {
515 return absl::DataLossError(absl::StrFormat(
516 "Project save failed (%s); changed plan preserved at %s",
517 status.message(), path));
518 }
519 std::error_code error;
520 if (!std::filesystem::remove(path, error) || error) {
521 return absl::DataLossError(absl::StrFormat(
522 "Project save failed (%s); plan cleanup failed at %s: %s",
523 status.message(), path, error.message()));
524 }
525 return status;
526 }
527#else
529 {project->filepath, path},
530 {"project plan", "project plan"}));
531 ASSIGN_OR_RETURN(auto before, ReadSource(project->filepath));
532 std::error_code error;
533 if (std::filesystem::exists(path, error)) {
534 return absl::AlreadyExistsError(
535 absl::StrFormat("Plan file already exists: %s", path));
536 }
537 if (error) {
538 return absl::FailedPreconditionError(error.message());
539 }
540 auto updated = *project;
541 updated.planning.file = updated.GetRelativePath(path);
542 updated.planning.section_present = true;
543 ASSIGN_OR_RETURN(auto descriptor, updated.SerializeToString());
546 {{project->filepath, before, descriptor},
547 {path, std::nullopt, plan.Serialize()}},
549 [&]() { return LoadFromFile(path).status(); }));
550#endif
551 project->planning = std::move(updated.planning);
552 return absl::OkStatus();
553}
554
555std::string ProjectPlan::Serialize() const {
556 PlanJson root = PlanJson::object();
557 root["schema"] = std::string(kProjectPlanSchema);
558 PutString(root, "title", title);
559 PutString(root, "about", about);
560
561 PlanJson goal_json = PlanJson::object();
562 PutString(goal_json, "title", goal.title);
563 PutString(goal_json, "detail", goal.detail);
564 PutString(goal_json, "due", goal.due);
565 PutExtras(goal_json, goal.extra);
566 if (!goal_json.empty()) {
567 root["goal"] = std::move(goal_json);
568 }
569
570 PlanJson milestones_json = PlanJson::array();
571 for (const auto& m : milestones) {
572 PlanJson j = PlanJson::object();
573 PutString(j, "id", m.id);
574 PutString(j, "title", m.title);
575 PutString(j, "start", m.start);
576 PutString(j, "end", m.end);
577 PutString(j, "status", m.status);
578 PutString(j, "detail", m.detail);
579 PutExtras(j, m.extra);
580 milestones_json.push_back(std::move(j));
581 }
582 root["milestones"] = std::move(milestones_json);
583
584 PlanJson route = PlanJson::object();
585 PlanJson steps = PlanJson::array();
586 for (const auto& s : route_steps) {
587 PlanJson j = PlanJson::object();
588 PutString(j, "id", s.id);
589 PutString(j, "code", s.code);
590 PutString(j, "title", s.title);
591 PutString(j, "note", s.note);
592 PutString(j, "group", s.group);
593 PutString(j, "kind", s.kind);
594 PutStrings(j, "targets", s.targets);
595 PutExtras(j, s.extra);
596 steps.push_back(std::move(j));
597 }
598 route["steps"] = std::move(steps);
599 PlanJson items = PlanJson::array();
600 for (const auto& item : route_items) {
601 PlanJson j = PlanJson::object();
602 PutString(j, "id", item.id);
603 PutString(j, "name", item.name);
604 PutString(j, "step", item.step);
605 PutString(j, "category", item.category);
606 j["required"] = item.required;
607 PutString(j, "status", item.status);
608 PutString(j, "where", item.where);
609 PutString(j, "note", item.note);
610 if (!item.needs.empty()) {
611 PlanJson needs = PlanJson::array();
612 for (const auto& need : item.needs) {
613 PlanJson n = PlanJson::object();
614 PutString(n, "step", need.step);
615 PutString(n, "what", need.what);
616 PutExtras(n, need.extra);
617 needs.push_back(std::move(n));
618 }
619 j["needs"] = std::move(needs);
620 }
621 PutExtras(j, item.extra);
622 items.push_back(std::move(j));
623 }
624 route["items"] = std::move(items);
625 PutExtras(route, route_extra);
626 root["route"] = std::move(route);
627
628 PlanJson decisions_json = PlanJson::array();
629 for (const auto& d : decisions) {
630 PlanJson j = PlanJson::object();
631 PutString(j, "id", d.id);
632 if (d.priority != 0) {
633 j["priority"] = d.priority;
634 }
635 PutString(j, "status", d.status);
636 PutString(j, "title", d.title);
637 PutString(j, "recommendation", d.recommendation);
638 PutString(j, "answer", d.answer);
639 PutStrings(j, "targets", d.targets);
640 PutStrings(j, "refs", d.refs);
641 PutExtras(j, d.extra);
642 decisions_json.push_back(std::move(j));
643 }
644 root["decisions"] = std::move(decisions_json);
645
646 PlanJson references_json = PlanJson::array();
647 for (const auto& r : references) {
648 PlanJson j = PlanJson::object();
649 PutString(j, "label", r.label);
650 PutString(j, "path", r.path);
651 PutString(j, "author", r.author);
652 PutString(j, "date", r.date);
653 PutExtras(j, r.extra);
654 references_json.push_back(std::move(j));
655 }
656 root["references"] = std::move(references_json);
657
658 PutExtras(root, extra);
659 return root.dump(1) + "\n";
660}
661
662absl::Status ProjectPlan::SaveToFile(const std::string& path) const {
664 return project::WriteProjectFileAtomically(path, Serialize(), false);
665}
666
667absl::Status ProjectPlan::SaveToFile(const std::string& path,
668 const std::string& expected_source) const {
670 return PublishSource(path, expected_source, Serialize());
671}
672
673absl::Status ProjectPlan::PublishSource(const std::string& path,
674 const std::string& expected_source,
675 const std::string& text) {
676 RETURN_IF_ERROR(Parse(text).status());
677#ifdef __EMSCRIPTEN__
678 ASSIGN_OR_RETURN(auto current, ReadSource(path));
679 if (current != expected_source) {
680 return absl::AbortedError("Plan changed since it was loaded");
681 }
682 return project::WriteProjectFileAtomically(path, text, true);
683#else
685 {path}, {"project plan", "project plan"}));
686 return PublishSourceArtifacts(*lock, {{path, expected_source, text}},
687 ComputeSourceArtifactSha256(expected_source),
688 [&]() { return LoadFromFile(path).status(); });
689#endif
690}
691
692absl::Status ProjectPlan::ValidateIds() const {
693 RETURN_IF_ERROR(core::ValidateIds(milestones, "Milestone"));
694 RETURN_IF_ERROR(core::ValidateIds(route_steps, "Route step"));
695 RETURN_IF_ERROR(core::ValidateIds(route_items, "Route item"));
696 return core::ValidateIds(decisions, "Decision");
697}
698
699std::string ProjectPlan::NewMilestoneId() const {
700 return UnusedId("m", EntryIds(milestones));
701}
702
703std::string ProjectPlan::NewStepId() const {
704 auto ids = EntryIds(route_steps);
705 for (const auto& item : route_items) {
706 ids.insert(item.step);
707 for (const auto& need : item.needs) {
708 ids.insert(need.step);
709 }
710 }
711 for (const auto& step : route_steps) {
712 for (const auto& target : step.targets) {
713 const auto parsed = PlanTarget::Parse(target);
714 if (parsed.kind == "step") {
715 ids.insert(parsed.id);
716 }
717 }
718 }
719 for (const auto& decision : decisions) {
720 for (const auto& target : decision.targets) {
721 const auto parsed = PlanTarget::Parse(target);
722 if (parsed.kind == "step") {
723 ids.insert(parsed.id);
724 }
725 }
726 }
727 return UnusedId("step-", ids);
728}
729
730std::string ProjectPlan::NewItemId() const {
731 auto ids = EntryIds(route_items);
732 for (const auto& step : route_steps) {
733 for (const auto& target : step.targets) {
734 const auto parsed = PlanTarget::Parse(target);
735 if (parsed.kind == "item") {
736 ids.insert(parsed.id);
737 }
738 }
739 }
740 for (const auto& decision : decisions) {
741 for (const auto& target : decision.targets) {
742 const auto parsed = PlanTarget::Parse(target);
743 if (parsed.kind == "item") {
744 ids.insert(parsed.id);
745 }
746 }
747 }
748 return UnusedId("item-", ids);
749}
750
751std::string ProjectPlan::NewDecisionId() const {
752 auto ids = EntryIds(decisions);
753 for (const auto& step : route_steps) {
754 for (const auto& target : step.targets) {
755 const auto parsed = PlanTarget::Parse(target);
756 if (parsed.kind == "decision") {
757 ids.insert(parsed.id);
758 }
759 }
760 }
761 for (const auto& decision : decisions) {
762 for (const auto& target : decision.targets) {
763 const auto parsed = PlanTarget::Parse(target);
764 if (parsed.kind == "decision") {
765 ids.insert(parsed.id);
766 }
767 }
768 }
769 return UnusedId("decision-", ids);
770}
771
772absl::Status ProjectPlan::RenameStep(std::string_view id, std::string new_id) {
774 const int index = FindStep(id);
775 if (index < 0) {
776 return absl::NotFoundError("Route step was not found");
777 }
778 if (new_id.empty()) {
779 return absl::InvalidArgumentError("Route step ID must be nonempty");
780 }
781 if (new_id != id && FindStep(new_id) >= 0) {
782 return absl::AlreadyExistsError(
783 "Route step ID must be nonempty and unique");
784 }
785 for (auto& item : route_items) {
786 if (item.step == id) {
787 item.step = new_id;
788 }
789 for (auto& need : item.needs) {
790 if (need.step == id) {
791 need.step = new_id;
792 }
793 }
794 }
795 for (auto& step : route_steps) {
796 RewriteTargets(&step.targets, "step:" + std::string(id), "step:" + new_id);
797 }
798 for (auto& decision : decisions) {
799 RewriteTargets(&decision.targets, "step:" + std::string(id),
800 "step:" + new_id);
801 }
802 RenamePlanAgentObservation(this, "step:" + std::string(id), "step:" + new_id);
803 route_steps[index].id = std::move(new_id);
804 return absl::OkStatus();
805}
806
807absl::Status ProjectPlan::RenameItem(std::string_view id, std::string new_id) {
809 auto item = std::find_if(route_items.begin(), route_items.end(),
810 [&](const auto& value) { return value.id == id; });
811 if (item == route_items.end()) {
812 return absl::NotFoundError("Route item was not found");
813 }
814 if (new_id.empty()) {
815 return absl::InvalidArgumentError("Route item ID must be nonempty");
816 }
817 if (new_id != id &&
818 std::any_of(route_items.begin(), route_items.end(),
819 [&](const auto& value) { return value.id == new_id; })) {
820 return absl::AlreadyExistsError(
821 "Route item ID must be nonempty and unique");
822 }
823 for (auto& step : route_steps) {
824 RewriteTargets(&step.targets, "item:" + std::string(id), "item:" + new_id);
825 }
826 for (auto& decision : decisions) {
827 RewriteTargets(&decision.targets, "item:" + std::string(id),
828 "item:" + new_id);
829 }
830 RenamePlanAgentObservation(this, "item:" + std::string(id), "item:" + new_id);
831 item->id = std::move(new_id);
832 return absl::OkStatus();
833}
834
835int ProjectPlan::FindStep(std::string_view id) const {
836 for (size_t i = 0; i < route_steps.size(); ++i) {
837 if (route_steps[i].id == id) {
838 return static_cast<int>(i);
839 }
840 }
841 return -1;
842}
843
844std::vector<const PlanRouteItem*> ProjectPlan::ItemsAtStep(
845 std::string_view step) const {
846 std::vector<const PlanRouteItem*> out;
847 for (const auto& item : route_items) {
848 if (item.step == step) {
849 out.push_back(&item);
850 }
851 }
852 return out;
853}
854
855std::vector<const PlanDecision*> ProjectPlan::OpenDecisions() const {
856 std::vector<const PlanDecision*> out;
857 for (const auto& d : decisions) {
858 if (d.status.empty() || d.status == "open") {
859 out.push_back(&d);
860 }
861 }
862 std::stable_sort(out.begin(), out.end(),
863 [](const PlanDecision* a, const PlanDecision* b) {
864 const int pa = a->priority > 0 ? a->priority : 1 << 30;
865 const int pb = b->priority > 0 ? b->priority : 1 << 30;
866 return pa < pb;
867 });
868 return out;
869}
870
872 std::string_view today) const {
873 for (const auto& m : milestones) {
874 if (m.status == "active") {
875 return &m;
876 }
877 }
878 for (const auto& m : milestones) {
879 if (!m.start.empty() && !m.end.empty() && m.start <= today &&
880 today <= m.end) {
881 return &m;
882 }
883 }
884 return nullptr;
885}
886
887} // namespace yaze::core
std::string Serialize() const
absl::Status SaveToFile(const std::string &path) const
static ProjectPlan CreateEmpty(std::string title)
std::string NewMilestoneId() const
static absl::StatusOr< ProjectPlan > Parse(std::string_view text)
static absl::Status CreateAndLink(project::YazeProject *project, const std::string &path)
std::vector< PlanReference > references
std::vector< const PlanRouteItem * > ItemsAtStep(std::string_view step) const
std::string NewStepId() const
const PlanMilestone * CurrentMilestone(std::string_view today) const
absl::Status ValidateIds() const
std::string NewDecisionId() const
std::vector< const PlanDecision * > OpenDecisions() const
std::vector< PlanMilestone > milestones
static absl::Status PublishSource(const std::string &path, const std::string &expected_source, const std::string &text)
std::string NewItemId() const
absl::Status RenameItem(std::string_view id, std::string new_id)
static absl::StatusOr< ProjectPlan > LoadFromFile(const std::string &path, std::string *source=nullptr)
std::vector< PlanRouteItem > route_items
int FindStep(std::string_view id) const
std::vector< PlanRouteStep > route_steps
std::vector< PlanDecision > decisions
absl::Status RenameStep(std::string_view id, std::string new_id)
const PlanJson & Array(const PlanJson &obj, const char *key)
void Int(const PlanJson &obj, const char *key, int *out)
void Strings(const PlanJson &obj, const char *key, std::vector< std::string > *out)
void Fail(const char *key, const char *expected)
bool RequireObject(const PlanJson &value, const std::string &where)
void Bool(const PlanJson &obj, const char *key, bool *out)
void String(const PlanJson &obj, const char *key, std::string *out)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status ReadMilestones(const PlanJson &root, std::vector< PlanMilestone > *out)
void PutExtras(PlanJson &obj, const PlanJson &extra)
absl::Status ReadRoute(const PlanJson &root, ProjectPlan *plan)
void RewriteTargets(std::vector< std::string > *targets, const std::string &before, const std::string &after)
absl::Status ValidateIds(const std::vector< Entry > &entries, const char *kind)
absl::Status ReadReferences(const PlanJson &root, std::vector< PlanReference > *out)
void PutString(PlanJson &obj, const char *key, const std::string &value)
absl::Status ReadDecisions(const PlanJson &root, std::vector< PlanDecision > *out)
void PutStrings(PlanJson &obj, const char *key, const std::vector< std::string > &values)
std::set< std::string > EntryIds(const std::vector< Entry > &entries)
absl::Status ReadGoal(const PlanJson &root, PlanGoal *goal)
std::string UnusedId(const char *prefix, const std::set< std::string > &ids)
PlanJson Extras(const PlanJson &obj, std::initializer_list< const char * > known)
absl::Status ReadNeeds(const PlanJson &item, const std::string &context, std::vector< PlanNeed > *out)
void RenamePlanAgentObservation(ProjectPlan *plan, std::string_view before, std::string_view after)
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
constexpr std::string_view kProjectPlanSchema
std::string ComputeSourceArtifactSha256(std::string_view content)
nlohmann::ordered_json PlanJson
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels, const std::vector< fs::path > &read_only_targets)
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:453
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< std::string > targets
std::vector< std::string > refs
std::vector< PlanNeed > needs
std::vector< std::string > targets
static PlanTarget Parse(std::string_view text)
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
PlanningSettings planning
Definition project.h:366