7#include <initializer_list>
11#include <system_error>
14#include "absl/strings/str_format.h"
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));
31 if (error || !std::filesystem::is_regular_file(status)) {
32 return absl::FailedPreconditionError(
33 absl::StrFormat(
"Not a readable regular file: %s", path));
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));
42 return absl::PermissionDeniedError(
43 absl::StrFormat(
"Cannot read file: %s", path));
45 std::stringstream buffer;
46 buffer << file.rdbuf();
48 return absl::DataLossError(
49 absl::StrFormat(
"Failed to read file: %s", path));
54template <
typename Entry>
55absl::Status
ValidateIds(
const std::vector<Entry>& entries,
const char* kind) {
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) {
66 if (invalid ==
nullptr) {
67 return absl::OkStatus();
69 return absl::InvalidArgumentError(absl::StrFormat(
70 "%s IDs must be nonempty and unique: %s", kind, invalid->id));
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) {
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)) {
93 const std::string& before,
const std::string& after) {
94 for (
auto& target : *targets) {
95 if (target == before) {
103 explicit Reader(std::string context) : context_(std::move(context)) {}
105 absl::Status
status()
const {
return status_; }
108 auto it = obj.find(key);
109 if (it == obj.end() || it->is_null()) {
112 if (!it->is_string()) {
113 Fail(key,
"a string");
116 *out = it->get<std::string>();
120 auto it = obj.find(key);
121 if (it == obj.end() || it->is_null()) {
124 if (!it->is_boolean()) {
125 Fail(key,
"true or false");
128 *out = it->get<
bool>();
132 auto it = obj.find(key);
133 if (it == obj.end() || it->is_null()) {
136 if (!it->is_number_integer()) {
137 Fail(key,
"an integer");
140 *out = it->get<
int>();
144 std::vector<std::string>* out) {
145 auto it = obj.find(key);
146 if (it == obj.end() || it->is_null()) {
149 if (!it->is_array()) {
150 Fail(key,
"an array of strings");
153 for (
const auto& value : *it) {
154 if (!value.is_string()) {
155 Fail(key,
"an array of strings");
158 out->push_back(value.get<std::string>());
164 static const PlanJson kEmpty = PlanJson::array();
165 auto it = obj.find(key);
166 if (it == obj.end() || it->is_null()) {
169 if (!it->is_array()) {
170 Fail(key,
"an array");
177 if (value.is_object()) {
181 status_ = absl::InvalidArgumentError(
182 absl::StrFormat(
"%s: %s must be an object", context_, where));
188 void Fail(
const char* key,
const char* expected) {
190 status_ = absl::InvalidArgumentError(
191 absl::StrFormat(
"%s: \"%s\" must be %s", context_, key, expected));
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; });
206 extra[it.key()] = it.value();
213 if (!value.empty()) {
219 const std::vector<std::string>& values) {
220 if (!values.empty()) {
226 if (!extra.is_object()) {
229 for (
auto it = extra.begin(); it != extra.end(); ++it) {
230 if (!obj.contains(it.key())) {
231 obj[it.key()] = it.value();
237 auto it = root.find(
"goal");
238 if (it == root.end() || it->is_null()) {
239 return absl::OkStatus();
248 goal->
extra =
Extras(*it, {
"title",
"detail",
"due"});
253 std::vector<PlanMilestone>* out) {
256 for (
size_t i = 0; i < list.size() && top.
status().ok(); ++i) {
257 Reader reader(absl::StrFormat(
"milestones[%d]", i));
262 reader.
String(list[i],
"id", &m.
id);
269 Extras(list[i], {
"id",
"title",
"start",
"end",
"status",
"detail"});
270 if (!reader.
status().ok()) {
273 out->push_back(std::move(m));
279 std::vector<PlanNeed>* out) {
282 for (
size_t i = 0; i < list.size() && top.
status().ok(); ++i) {
283 Reader reader(absl::StrFormat(
"%s.needs[%d]", context, i));
291 if (!reader.
status().ok()) {
294 out->push_back(std::move(need));
300 auto it = root.find(
"route");
301 if (it == root.end() || it->is_null()) {
302 return absl::OkStatus();
312 for (
size_t i = 0; i < steps.size() && top.
status().ok(); ++i) {
313 Reader reader(absl::StrFormat(
"route.steps[%d]", i));
318 reader.
String(steps[i],
"id", &step.
id);
326 steps[i], {
"id",
"code",
"title",
"note",
"group",
"kind",
"targets"});
327 if (!reader.
status().ok()) {
334 for (
size_t i = 0; i < items.size() && top.
status().ok(); ++i) {
335 const std::string context = absl::StrFormat(
"route.items[%d]", i);
341 reader.
String(items[i],
"id", &item.
id);
349 item.
extra =
Extras(items[i], {
"id",
"name",
"step",
"category",
"required",
350 "status",
"where",
"note",
"needs"});
351 if (!reader.
status().ok()) {
355 if (!needs_status.ok()) {
364 std::vector<PlanDecision>* out) {
367 for (
size_t i = 0; i < list.size() && top.
status().ok(); ++i) {
368 Reader reader(absl::StrFormat(
"decisions[%d]", i));
373 reader.
String(list[i],
"id", &d.
id);
381 d.
extra =
Extras(list[i], {
"id",
"title",
"status",
"priority",
382 "recommendation",
"answer",
"targets",
"refs"});
383 if (!reader.
status().ok()) {
386 out->push_back(std::move(d));
392 std::vector<PlanReference>* out) {
395 for (
size_t i = 0; i < list.size() && top.
status().ok(); ++i) {
396 Reader reader(absl::StrFormat(
"references[%d]", i));
405 r.
extra =
Extras(list[i], {
"label",
"path",
"author",
"date"});
406 if (!reader.
status().ok()) {
409 out->push_back(std::move(r));
418 const size_t colon = text.find(
':');
419 if (colon == std::string_view::npos) {
420 target.
id = std::string(text);
423 target.
kind = std::string(text.substr(0, colon));
424 target.
id = std::string(text.substr(colon + 1));
429 if ((
kind !=
"room" &&
kind !=
"map") ||
id.empty()) {
433 const long value = std::strtol(
id.c_str(), &end, 0);
434 if (end ==
id.c_str() || *end !=
'\0' || value < 0) {
437 return static_cast<int>(value);
447 PlanJson root = PlanJson::parse(text.begin(), text.end(),
nullptr,
449 if (root.is_discarded()) {
450 return absl::InvalidArgumentError(
"Plan file is not valid JSON");
452 if (!root.is_object()) {
453 return absl::InvalidArgumentError(
"Plan file must be a JSON object");
455 auto schema = root.find(
"schema");
456 if (schema == root.end() || !schema->is_string() ||
458 return absl::InvalidArgumentError(
459 absl::StrFormat(
"Plan file must declare \"schema\": \"%s\"",
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();
471 {ReadGoal(root, &plan.
goal), ReadMilestones(root, &plan.
milestones),
472 ReadRoute(root, &plan), ReadDecisions(root, &plan.
decisions),
478 plan.
extra = Extras(root, {
"schema",
"title",
"about",
"goal",
"milestones",
479 "route",
"decisions",
"references"});
485 std::string* source) {
487 auto plan =
Parse(contents);
490 plan.status().code(),
491 absl::StrFormat(
"%s: %s", path, plan.status().message()));
493 if (source !=
nullptr) {
494 *source = std::move(contents);
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");
508 auto updated = *project;
510 updated.planning.section_present =
true;
511 const auto status = updated.Save();
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));
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()));
530 {
"project plan",
"project plan"}));
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));
538 return absl::FailedPreconditionError(error.message());
540 auto updated = *project;
542 updated.planning.section_present =
true;
546 {{project->
filepath, before, descriptor},
547 {path, std::nullopt, plan.Serialize()}},
551 project->
planning = std::move(updated.planning);
552 return absl::OkStatus();
558 PutString(root,
"title",
title);
559 PutString(root,
"about",
about);
561 PlanJson goal_json = PlanJson::object();
562 PutString(goal_json,
"title",
goal.
title);
564 PutString(goal_json,
"due",
goal.
due);
566 if (!goal_json.empty()) {
567 root[
"goal"] = std::move(goal_json);
570 PlanJson milestones_json = PlanJson::array();
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));
582 root[
"milestones"] = std::move(milestones_json);
584 PlanJson route = 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));
598 route[
"steps"] = std::move(steps);
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()) {
612 for (
const auto& need : item.needs) {
614 PutString(n,
"step", need.step);
615 PutString(n,
"what", need.what);
616 PutExtras(n, need.extra);
617 needs.push_back(std::move(n));
619 j[
"needs"] = std::move(needs);
621 PutExtras(j, item.extra);
622 items.push_back(std::move(j));
624 route[
"items"] = std::move(items);
626 root[
"route"] = std::move(route);
628 PlanJson decisions_json = PlanJson::array();
631 PutString(j,
"id", d.id);
632 if (d.priority != 0) {
633 j[
"priority"] = d.priority;
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));
644 root[
"decisions"] = std::move(decisions_json);
646 PlanJson references_json = PlanJson::array();
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));
656 root[
"references"] = std::move(references_json);
658 PutExtras(root,
extra);
659 return root.dump(1) +
"\n";
668 const std::string& expected_source)
const {
674 const std::string& expected_source,
675 const std::string& text) {
679 if (current != expected_source) {
680 return absl::AbortedError(
"Plan changed since it was loaded");
685 {path}, {
"project plan",
"project plan"}));
696 return core::ValidateIds(
decisions,
"Decision");
706 ids.insert(item.step);
707 for (
const auto& need : item.needs) {
708 ids.insert(need.step);
712 for (
const auto& target : step.targets) {
714 if (parsed.kind ==
"step") {
715 ids.insert(parsed.id);
720 for (
const auto& target : decision.targets) {
722 if (parsed.kind ==
"step") {
723 ids.insert(parsed.id);
727 return UnusedId(
"step-", ids);
733 for (
const auto& target : step.targets) {
735 if (parsed.kind ==
"item") {
736 ids.insert(parsed.id);
741 for (
const auto& target : decision.targets) {
743 if (parsed.kind ==
"item") {
744 ids.insert(parsed.id);
748 return UnusedId(
"item-", ids);
754 for (
const auto& target : step.targets) {
756 if (parsed.kind ==
"decision") {
757 ids.insert(parsed.id);
762 for (
const auto& target : decision.targets) {
764 if (parsed.kind ==
"decision") {
765 ids.insert(parsed.id);
769 return UnusedId(
"decision-", ids);
776 return absl::NotFoundError(
"Route step was not found");
778 if (new_id.empty()) {
779 return absl::InvalidArgumentError(
"Route step ID must be nonempty");
781 if (new_id !=
id &&
FindStep(new_id) >= 0) {
782 return absl::AlreadyExistsError(
783 "Route step ID must be nonempty and unique");
786 if (item.step ==
id) {
789 for (
auto& need : item.needs) {
790 if (need.step ==
id) {
796 RewriteTargets(&step.targets,
"step:" + std::string(
id),
"step:" + new_id);
799 RewriteTargets(&decision.targets,
"step:" + std::string(
id),
804 return absl::OkStatus();
810 [&](
const auto& value) { return value.id == id; });
812 return absl::NotFoundError(
"Route item was not found");
814 if (new_id.empty()) {
815 return absl::InvalidArgumentError(
"Route item ID must be nonempty");
819 [&](
const auto& value) { return value.id == new_id; })) {
820 return absl::AlreadyExistsError(
821 "Route item ID must be nonempty and unique");
824 RewriteTargets(&step.targets,
"item:" + std::string(
id),
"item:" + new_id);
827 RewriteTargets(&decision.targets,
"item:" + std::string(
id),
831 item->id = std::move(new_id);
832 return absl::OkStatus();
838 return static_cast<int>(i);
845 std::string_view step)
const {
846 std::vector<const PlanRouteItem*> out;
848 if (item.step == step) {
849 out.push_back(&item);
856 std::vector<const PlanDecision*> out;
858 if (d.status.empty() || d.status ==
"open") {
862 std::stable_sort(out.begin(), out.end(),
864 const int pa = a->priority > 0 ? a->priority : 1 << 30;
865 const int pb = b->priority > 0 ? b->priority : 1 << 30;
872 std::string_view today)
const {
874 if (m.status ==
"active") {
879 if (!m.start.empty() && !m.end.empty() && m.start <= today &&
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)
Reader(std::string context)
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)
absl::Status status() const
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)
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)
#define RETURN_IF_ERROR(expr)
std::vector< std::string > targets
std::vector< std::string > refs
std::string recommendation
std::vector< PlanNeed > needs
std::vector< std::string > targets
static PlanTarget Parse(std::string_view text)
Modern project structure with comprehensive settings consolidation.
PlanningSettings planning