5#include <initializer_list>
8#include "absl/strings/ascii.h"
9#include "absl/strings/str_format.h"
18constexpr const char*
kStampKeys[] = {
"agent",
"observed_utc"};
21 return text.size() == 64 &&
22 std::all_of(text.begin(), text.end(), [](
unsigned char c) {
23 return std::isdigit(c) || (c >=
'a' && c <=
'f');
29 const bool known_kind =
31 [&](
const char* kind) {
return target.
kind == kind; });
32 if (!known_kind || target.
id.empty()) {
33 return absl::InvalidArgumentError(absl::StrFormat(
34 "Observation key must be step:, item:, decision: or milestone: "
35 "followed by an ID: \"%s\"",
41template <
typename Entry>
42bool HasId(
const std::vector<Entry>& entries,
const std::string&
id) {
43 return std::any_of(entries.begin(), entries.end(),
44 [&](
const Entry& entry) { return entry.id == id; });
48 if (target.
kind ==
"step") {
51 if (target.
kind ==
"item") {
54 if (target.
kind ==
"decision") {
57 if (target.
kind ==
"milestone") {
64 if (!obs.is_object()) {
65 return absl::InvalidArgumentError(
66 absl::StrFormat(
"Observation \"%s\" must be an object", key));
68 for (
const char* field : {
"state",
"summary",
"next"}) {
69 auto it = obs.find(field);
70 if (it != obs.end() && !it->is_string()) {
71 return absl::InvalidArgumentError(absl::StrFormat(
72 "Observation \"%s\": \"%s\" must be a string", key, field));
75 if (
auto it = obs.find(
"evidence"); it != obs.end()) {
76 if (!it->is_array() ||
77 !std::all_of(it->begin(), it->end(),
78 [](
const PlanJson& v) { return v.is_string(); })) {
79 return absl::InvalidArgumentError(absl::StrFormat(
80 "Observation \"%s\": \"evidence\" must be an array of strings", key));
83 if (
auto it = obs.find(
"prs"); it != obs.end()) {
84 if (!it->is_array() ||
85 !std::all_of(it->begin(), it->end(), [](
const PlanJson& v) {
86 return v.is_number_integer() || v.is_string();
88 return absl::InvalidArgumentError(absl::StrFormat(
89 "Observation \"%s\": \"prs\" must be an array of numbers or strings",
93 return absl::OkStatus();
98 nlohmann::json facts = nlohmann::json::parse(obs.dump());
100 facts.erase(std::string(stamp));
107 if (it == plan.
extra.end()) {
108 return static_cast<const PlanJson*
>(
nullptr);
110 if (!it->is_object()) {
111 return absl::FailedPreconditionError(
112 "The plan's \"agent_status\" section is not an object; repair it "
113 "before applying agent updates");
115 auto observations = it->find(
"observations");
116 if (observations != it->end() && !observations->is_object()) {
117 return absl::FailedPreconditionError(
118 "The plan's \"agent_status.observations\" is not an object; repair "
119 "it before applying agent updates");
126 auto it = root.find(key);
127 if (it == root.end() || it->is_null()) {
128 return absl::OkStatus();
130 if (!it->is_string()) {
131 return absl::InvalidArgumentError(
132 absl::StrFormat(
"Agent update \"%s\" must be a string", key));
134 *out = it->get<std::string>();
135 return absl::OkStatus();
141 PlanJson root = PlanJson::parse(text.begin(), text.end(),
nullptr,
143 if (root.is_discarded() || !root.is_object()) {
144 return absl::InvalidArgumentError(
"Agent update must be a JSON object");
146 auto schema = root.find(
"schema");
147 if (schema == root.end() || !schema->is_string() ||
149 return absl::InvalidArgumentError(
150 absl::StrFormat(
"Agent update must declare \"schema\": \"%s\"",
153 for (
auto it = root.begin(); it != root.end(); ++it) {
154 static constexpr const char* kKnown[] = {
155 "schema",
"base_sha256",
"agent",
"observed_utc",
156 "sources",
"observations",
"remove"};
157 if (std::none_of(std::begin(kKnown), std::end(kKnown),
158 [&](
const char* key) {
return it.key() == key; })) {
159 return absl::InvalidArgumentError(absl::StrFormat(
160 "Agent update has an unknown field \"%s\"", it.key()));
168 return absl::InvalidArgumentError(
169 "Agent update \"base_sha256\" must be 64 hexadecimal characters");
173 ReadOptionalString(root,
"observed_utc", &update.
observed_utc));
175 if (
auto it = root.find(
"sources"); it != root.end() && !it->is_null()) {
176 if (!it->is_object()) {
177 return absl::InvalidArgumentError(
178 "Agent update \"sources\" must be an object");
182 if (
auto it = root.find(
"observations"); it != root.end() && !it->is_null()) {
183 if (!it->is_object()) {
184 return absl::InvalidArgumentError(
185 "Agent update \"observations\" must be an object");
189 if (
auto it = root.find(
"remove"); it != root.end() && !it->is_null()) {
190 if (!it->is_array()) {
191 return absl::InvalidArgumentError(
192 "Agent update \"remove\" must be an array of observation keys");
194 for (
const auto& key : *it) {
195 if (!key.is_string()) {
196 return absl::InvalidArgumentError(
197 "Agent update \"remove\" must be an array of observation keys");
199 update.
remove.push_back(key.get<std::string>());
207 std::any_of(
changes.begin(),
changes.end(), [](
const auto& change) {
208 return change.kind != PlanAgentChangeKind::kUnchanged;
229 static const PlanJson kEmpty = PlanJson::object();
231 if (section !=
nullptr) {
232 if (
auto it = section->find(
"observations"); it != section->end()) {
240 const std::string& key = it.key();
242 if (!PlanHasTarget(plan, target)) {
243 return absl::NotFoundError(absl::StrFormat(
244 "The plan has no %s with ID \"%s\"; agent updates use existing "
249 if (std::find(update.
remove.begin(), update.
remove.end(), key) !=
251 return absl::InvalidArgumentError(absl::StrFormat(
252 "Observation \"%s\" is both updated and removed", key));
254 auto current = stored->find(key);
256 if (current != stored->end()) {
257 kind = Facts(*current) == Facts(it.value())
261 diff.
changes.push_back({key, kind});
263 for (
const auto& key : update.
remove) {
265 if (stored->contains(key)) {
270 const PlanJson* current_sources =
nullptr;
271 if (section !=
nullptr) {
272 if (
auto it = section->find(
"sources"); it != section->end()) {
273 current_sources = &*it;
277 nlohmann::json::parse(current_sources->dump()) !=
278 nlohmann::json::parse(update.
sources.dump());
285 if (plan ==
nullptr) {
286 return absl::InvalidArgumentError(
"A plan is required");
293 PlanJson section = existing !=
nullptr ? *existing : PlanJson::object();
298 if (!update.
agent.empty()) {
299 section[
"agent"] = update.
agent;
302 section[
"sources"] = update.
sources;
304 PlanJson& observations = section[
"observations"];
305 if (observations.is_null()) {
306 observations = PlanJson::object();
308 for (
const auto& change : diff.
changes) {
309 switch (change.kind) {
313 for (
const char* stamp : kStampKeys) {
314 obs.erase(std::string(stamp));
316 if (!update.
agent.empty()) {
317 obs[
"agent"] = update.
agent;
322 observations[change.key] = std::move(obs);
326 observations.erase(change.key);
337 const std::string& path,
const PlanAgentUpdate& update,
bool dry_run) {
339 return absl::InvalidArgumentError(
340 "Agent update needs \"base_sha256\" (the plan bytes it read) before "
341 "it can be applied");
349 return absl::FailedPreconditionError(absl::StrFormat(
350 "Plan changed since the agent read it (base %s, current %s); reread "
351 "the plan and rebuild the update. Nothing was written.",
361 PlanJson root = PlanJson::parse(source,
nullptr,
false);
363 root[key] = plan.
extra.at(key);
364 const std::string text = root.dump(1) +
"\n";
366 if (!written.ok() || written->Serialize() != plan.
Serialize()) {
367 return absl::InternalError(
368 "Agent update did not reproduce the validated plan; nothing was "
382 return it != plan.
extra.end() && it->is_object() ? &*it :
nullptr;
386 std::string_view key) {
388 if (section ==
nullptr) {
391 auto observations = section->find(
"observations");
392 if (observations == section->end() || !observations->is_object()) {
395 auto it = observations->find(std::string(key));
396 return it != observations->end() && it->is_object() ? &*it :
nullptr;
401 std::vector<std::string> orphans;
403 if (section ==
nullptr) {
406 auto observations = section->find(
"observations");
407 if (observations == section->end() || !observations->is_object()) {
410 for (
auto it = observations->begin(); it != observations->end(); ++it) {
411 auto target = ParseObservationKey(it.key());
412 if (!target.ok() || !PlanHasTarget(plan, *target)) {
413 orphans.push_back(it.key());
420 std::string_view after) {
422 if (section == plan->
extra.end() || !section->is_object()) {
425 auto observations = section->find(
"observations");
426 if (observations == section->end() || !observations->is_object()) {
429 auto it = observations->find(std::string(before));
430 if (it == observations->end() || before == after) {
434 observations->erase(std::string(before));
435 (*observations)[std::string(after)] = std::move(moved);
439 std::string_view loaded_source,
440 std::string_view disk_source) {
441 if (draft ==
nullptr) {
446 if (!loaded.ok() || !disk.ok()) {
451 const bool disk_has_section = disk->extra.contains(key);
452 if (disk_has_section) {
453 disk_section = disk->extra[key];
455 loaded->extra.erase(key);
456 disk->extra.erase(key);
457 if (loaded->Serialize() != disk->Serialize()) {
460 if (disk_has_section) {
461 draft->
extra[key] = std::move(disk_section);
463 draft->
extra.erase(key);
std::string Serialize() const
static absl::StatusOr< ProjectPlan > Parse(std::string_view text)
std::vector< PlanMilestone > milestones
static absl::Status PublishSource(const std::string &path, const std::string &expected_source, const std::string &text)
static absl::StatusOr< ProjectPlan > LoadFromFile(const std::string &path, std::string *source=nullptr)
std::vector< PlanRouteItem > route_items
std::vector< PlanRouteStep > route_steps
std::vector< PlanDecision > decisions
#define ASSIGN_OR_RETURN(type_variable_name, expression)
bool PlanHasTarget(const ProjectPlan &plan, const PlanTarget &target)
absl::Status ReadOptionalString(const PlanJson &root, const char *key, std::string *out)
absl::Status ValidateObservation(const std::string &key, const PlanJson &obs)
constexpr const char * kStampKeys[]
nlohmann::json Facts(const PlanJson &obs)
bool HasId(const std::vector< Entry > &entries, const std::string &id)
absl::StatusOr< const PlanJson * > ExistingSection(const ProjectPlan &plan)
absl::StatusOr< PlanTarget > ParseObservationKey(std::string_view key)
bool IsSha256(std::string_view text)
constexpr const char * kTargetKinds[]
void RenamePlanAgentObservation(ProjectPlan *plan, std::string_view before, std::string_view after)
const PlanJson * FindPlanAgentStatus(const ProjectPlan &plan)
const PlanJson * FindPlanAgentObservation(const ProjectPlan &plan, std::string_view key)
constexpr std::string_view kPlanAgentUpdateSchema
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< PlanAgentDiff > ApplyPlanAgentUpdate(ProjectPlan *plan, const PlanAgentUpdate &update)
absl::StatusOr< PlanAgentDiff > DiffPlanAgentUpdate(const ProjectPlan &plan, const PlanAgentUpdate &update)
constexpr std::string_view kPlanAgentStatusSchema
bool AdoptPlanAgentOnlyChanges(ProjectPlan *draft, std::string_view loaded_source, std::string_view disk_source)
const char * PlanAgentChangeKindName(PlanAgentChangeKind kind)
nlohmann::ordered_json PlanJson
absl::StatusOr< PlanAgentFileResult > ApplyPlanAgentUpdateToFile(const std::string &path, const PlanAgentUpdate &update, bool dry_run)
std::vector< std::string > OrphanedPlanAgentObservations(const ProjectPlan &plan)
constexpr std::string_view kPlanAgentStatusKey
#define RETURN_IF_ERROR(expr)
std::vector< PlanAgentChange > changes
std::string before_sha256
std::vector< std::string > remove
static absl::StatusOr< PlanAgentUpdate > Parse(std::string_view text)
static PlanTarget Parse(std::string_view text)