Creation — refused by default, opted in per parent (L4 graph)¶
A data write to an address that does not resolve answers NOT_FOUND and creates nothing,
locally and from a peer alike
(RFC-0030
§7.1). An application that wants a write to create, below one parent, installs a creation
hook there with graph_t::set_creation_hook (§7.2). The hook is shown the missing child’s
key, the writer’s subject and the payload, and it registers the child itself, typed as the
application decides, or refuses.
What to notice¶
Refusal is the default everywhere. The first write fails with
NOT_FOUNDand leaves no intermediate level behind. A remoteFWD{WRITE}gets the same answer; that arm is pinned incore/tests/op_resolve_test.cpp.The hook is compiled out by default.
config_t::kCreationHooksisfalseon every profile, so a lean build has no hook slot:set_creation_hookanswersSCHEMA_NOT_FOUNDand every miss stays refused. The example checks that and stops there in such a build. Setstatic constexpr bool kCreationHooks = true;in yourlibtracer/config_override.hppto opt in.The parent’s
CREATEright is checked before the hook runs. A writer the parent’s ACL denies getsPermissionDenied, and the hook never sees the write.One level per hook. The hook on
/zonecreates/zone/a, which carries no hook of its own, so the next level,/zone/a/b, is refused.mkdir -pholds only where every level opted in.The
:control plane never creates. A field write to a nonexistent vertex isNOT_FOUND, because there is no vertex to control.Appearance is the first write. A child the hook creates appears as the write that caused it, so a subtree subscriber above it sees the child arrive without any dedicated event type (reference 02 §Observing structural change).
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief ONE CONCEPT — creation is refused by default, and a parent opts in with app logic.
9 *
10 * A data write to an address that does not resolve answers `NOT_FOUND` and creates nothing,
11 * locally and from a peer alike (RFC-0030 §7.1). An application that wants "a write creates"
12 * below one parent installs a CREATION HOOK there (`graph_t::set_creation_hook`, RFC-0030
13 * §7.2): the hook is shown the missing child's key and the payload, and registers the child
14 * itself, typed as it decides, or refuses.
15 *
16 * Whether a vertex can carry a hook at all is the compile-time policy `config_t::kCreationHooks`,
17 * off by default. In a build that leaves it off, the install is refused with `SCHEMA_NOT_FOUND`
18 * and every miss stays refused; the example checks that and stops there.
19 *
20 * Runs under ctest as `example_graph_creation_hook`; returns non-zero on any failed check.
21 */
22
23#include <cstdio>
24#include <span>
25#include <string_view>
26#include <vector>
27
28#include "libtracer/tracer.hpp"
29
30namespace {
31
32using tr::graph::path_t;
33using tr::graph::result_t;
34using tr::graph::role_t;
35using tr::graph::status_t;
36
37/** @brief An owned one-segment view over @p text. */
38tr::view::view_t value_of(std::string_view text) {
39 return *tr::view::over_bytes(std::as_bytes(std::span<const char>(text.data(), text.size())));
40}
41
42/** @brief Report expectation @p what and record a failure on @p ok. */
43void check(bool& ok, bool cond, const char* what) {
44 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
45 ok = ok && cond;
46}
47
48/** @brief The app's creation logic: every missing child of its parent becomes a stored value. */
49result_t<void> create_stored(void* ctx, tr::graph::vertex_handle_t /*parent*/,
50 std::span<const std::byte> child_key, std::string_view /*subject*/,
51 const tr::view::rope_t& /*payload*/) {
52 auto& g = *static_cast<tr::graph::graph_t*>(ctx);
53 const auto made = g.register_vertex_key(
54 std::vector<std::byte>(child_key.begin(), child_key.end()), role_t::STORED_VALUE);
55 if (!made) return std::unexpected(made.error());
56 return {};
57}
58
59} // namespace
60
61int main() {
62 tr::graph::graph_t g;
63 bool ok = true;
64
65 // Nothing is registered, and nothing is created by writing.
66 const auto w = g.write(path_t("/zone/soil"), value_of("moist"));
67 check(ok, !w && w.error() == status_t::NOT_FOUND,
68 "a data write to an unresolved path is NOT_FOUND");
69 check(ok, !g.find(path_t("/zone").key()), "and it created no intermediate level");
70
71 // Field writes never create either — there is no vertex to control.
72 const auto fw = g.write(path_t("/zone/b:acl"), value_of("x"));
73 check(ok, !fw && fw.error() == status_t::NOT_FOUND,
74 "a :field write to a nonexistent vertex is NOT_FOUND");
75
76 // Opt /zone in. A build without creation hooks refuses the install by value.
77 const auto zone = g.register_vertex(path_t("/zone"), role_t::STORED_VALUE);
78 const auto installed = g.set_creation_hook(zone, {&create_stored, &g});
79 if (!tr::graph::kCreationHooks) {
80 check(ok, !installed && installed.error() == status_t::SCHEMA_NOT_FOUND,
81 "this build has no hook slot, so the install is refused");
82 return ok ? 0 : 1;
83 }
84 check(ok, installed.has_value(), "the app installs a creation hook on /zone");
85 check(ok, g.write(path_t("/zone/soil"), value_of("moist")).has_value(),
86 "a write to the missing /zone/soil now creates it, through the hook");
87 check(ok, g.read(path_t("/zone/soil")).has_value(),
88 "the created vertex serves the value that created it");
89
90 // The hook decides one level. It creates /zone/a, which carries no hook of its own, so the
91 // next level, /zone/a/b, is refused: `mkdir -p` holds only where every level opted in.
92 const auto deep = g.write(path_t("/zone/a/b"), value_of("x"));
93 check(ok, !deep && deep.error() == status_t::NOT_FOUND,
94 "a deeper miss is decided by the new child's own hook, and it has none");
95 return ok ? 0 : 1;
96}
See also: graph module · graph model reference §Vertex lifecycle · retirement (the other half of the lifecycle).