nx_relate.nx source
↩ module page · 171 lines · 10509 B
1// nx_relate.nx -- THE UNIFIED RELATIONSHIP GRAPH (rung 1 of Nishi CRM/Relationships to SOTA).
2// Operator 2026-07-08: "more than just crm ... a personal AND a professional aka network of people so
3// relationships across everything can be managed -- even church or other orgs or whatever".
4// ONE graph, typed:
5// ENTITY-TYPE : person | company | org | household (people AND the things they belong to)
6// REL-EDGE : typed relationship between two entities (works-at | member-of | family | reports-to |
7// donor-to | friend | colleague) -- semantics-checked (works-at needs a company, member-of
8// needs an org/household, family needs two people, ...)
9// REL-ROLE : the role on that edge (title / position / ministry) -- e.g. Managing Partner, Relief Society President
10// REL-CONTEXT : which world the relationship lives in (personal | professional | church | civic | community)
11// HOUSEHOLD : a family/household is a first-class entity that groups people
12// CUSTOM-FIELD: arbitrary key=value on any entity (flexible schema, Attio-style)
13// TENANT : every entity is scoped to a workspace/tenant, so clients like andelinwest get isolation
14// So one person can be family + a church member + an employee at once -- across everything, in ONE graph.
15// Integrates with nx_send (a send-intent targets any entity) + Vizsla contacts/timeline/reminders.
16// Pure logic + a self-test gate (argless = selftest). Persistence + UI = next rungs.
17// Commands: entity <tenant> <type> <name> | edge <tenant> <fromType> <fromName> <kind> <toType> <toName> <context> [role] | field <tenant> <entity> <key> <value> | selftest
18// expect_exit: 0 license_tier: ORIGINAL
19import "nx_syscalls.nx"
20
21func slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
22func p(s: *u8) -> i64 { sys_write(1, s, slen(s)); return 0 }
23func seq(a: *u8, b: *u8) -> i64 {
24 var i: i64 = 0
25 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
26 if b[i] != (0 as u8) { return 0 }
27 return 1
28}
29func is_empty(s: *u8) -> i64 { if s[0] == (0 as u8) { return 1 } return 0 }
30
31// ---- ENTITY-TYPE ----
32func re_valid_type(t: *u8) -> i64 {
33 if seq(t, "person" as *u8) == 1 { return 1 }
34 if seq(t, "company" as *u8) == 1 { return 1 }
35 if seq(t, "org" as *u8) == 1 { return 1 }
36 if seq(t, "household" as *u8) == 1 { return 1 }
37 return 0
38}
39// ---- REL-EDGE kinds ----
40func re_valid_kind(k: *u8) -> i64 {
41 if seq(k, "works-at" as *u8) == 1 { return 1 }
42 if seq(k, "member-of" as *u8) == 1 { return 1 }
43 if seq(k, "family" as *u8) == 1 { return 1 }
44 if seq(k, "reports-to" as *u8) == 1 { return 1 }
45 if seq(k, "donor-to" as *u8) == 1 { return 1 }
46 if seq(k, "friend" as *u8) == 1 { return 1 }
47 if seq(k, "colleague" as *u8) == 1 { return 1 }
48 if seq(k, "recruits" as *u8) == 1 { return 1 } // recruiter --recruits--> candidate (career ring)
49 return 0
50}
51// ---- REL-CONTEXT ----
52func re_valid_context(c: *u8) -> i64 {
53 if seq(c, "personal" as *u8) == 1 { return 1 }
54 if seq(c, "professional" as *u8) == 1 { return 1 }
55 if seq(c, "church" as *u8) == 1 { return 1 }
56 if seq(c, "civic" as *u8) == 1 { return 1 }
57 if seq(c, "community" as *u8) == 1 { return 1 }
58 return 0
59}
60// edge semantics: are these entity types compatible with this relationship kind?
61func re_edge_ok(fromType: *u8, kind: *u8, toType: *u8) -> i64 {
62 if re_valid_kind(kind) == 0 { return 0 }
63 if seq(kind, "works-at" as *u8) == 1 { if seq(fromType, "person" as *u8) == 1 { if seq(toType, "company" as *u8) == 1 { return 1 } } return 0 }
64 if seq(kind, "member-of" as *u8) == 1 { if seq(fromType, "person" as *u8) == 1 { if seq(toType, "org" as *u8) == 1 { return 1 } if seq(toType, "household" as *u8) == 1 { return 1 } } return 0 }
65 if seq(kind, "donor-to" as *u8) == 1 { if seq(fromType, "person" as *u8) == 1 { if seq(toType, "org" as *u8) == 1 { return 1 } } return 0 }
66 // family | reports-to | friend | colleague are person <-> person
67 if seq(fromType, "person" as *u8) == 1 { if seq(toType, "person" as *u8) == 1 { return 1 } }
68 return 0
69}
70
71func cmd_entity(tenant: *u8, t: *u8, name: *u8) -> i64 {
72 if re_valid_type(t) == 0 { p("ENTITY-TYPE ERROR unknown type=" as *u8); p(t); p(" (person|company|org|household) -- fail loud\n" as *u8); return 1 }
73 p("ENTITY-TYPE tenant=" as *u8); p(tenant); p(" type=" as *u8); p(t); p(" name=" as *u8); p(name); p("\n" as *u8)
74 return 0
75}
76func cmd_edge(tenant: *u8, fromType: *u8, fromName: *u8, kind: *u8, toType: *u8, toName: *u8, context: *u8, role: *u8) -> i64 {
77 var ok: i64 = 1
78 var why: *u8 = "ok" as *u8
79 if re_valid_type(fromType) == 0 { ok = 0; why = "bad from-entity type" as *u8 }
80 if re_valid_type(toType) == 0 { ok = 0; why = "bad to-entity type" as *u8 }
81 if re_valid_kind(kind) == 0 { ok = 0; why = "unknown relationship kind" as *u8 }
82 if re_valid_context(context) == 0 { ok = 0; why = "unknown context" as *u8 }
83 if ok == 1 { if re_edge_ok(fromType, kind, toType) == 0 { ok = 0; why = "entity types incompatible with this relationship" as *u8 } }
84 p("REL-EDGE tenant=" as *u8); p(tenant); p(" " as *u8); p(fromType); p(":" as *u8); p(fromName)
85 p(" --[" as *u8); p(kind); p("]--> " as *u8); p(toType); p(":" as *u8); p(toName)
86 p(" REL-CONTEXT=" as *u8); p(context); p(" REL-ROLE=" as *u8); p(role)
87 if ok == 1 { p(" status=ok\n" as *u8); return 0 }
88 p(" status=REJECTED (" as *u8); p(why); p(")\n" as *u8)
89 return 1
90}
91func cmd_field(tenant: *u8, entity: *u8, key: *u8, val: *u8) -> i64 {
92 if is_empty(key) == 1 { p("CUSTOM-FIELD ERROR empty key -- fail loud\n" as *u8); return 1 }
93 p("CUSTOM-FIELD tenant=" as *u8); p(tenant); p(" entity=" as *u8); p(entity); p(" " as *u8); p(key); p("=" as *u8); p(val); p("\n" as *u8)
94 return 0
95}
96
97func cmd_selftest() -> i64 {
98 p("=== NX-RELATE SELFTEST (ENTITY-TYPE + REL-EDGE + REL-ROLE + REL-CONTEXT + HOUSEHOLD + CUSTOM-FIELD + TENANT) ===\n" as *u8)
99 var ok: i64 = 1
100 // entity types (pos + neg)
101 if re_valid_type("person" as *u8) != 1 { ok = 0 }
102 if re_valid_type("company" as *u8) != 1 { ok = 0 }
103 if re_valid_type("org" as *u8) != 1 { ok = 0 }
104 if re_valid_type("household" as *u8) != 1 { ok = 0 }
105 if re_valid_type("teammm" as *u8) != 0 { ok = 0 }
106 // kinds
107 if re_valid_kind("works-at" as *u8) != 1 { ok = 0 }
108 if re_valid_kind("member-of" as *u8) != 1 { ok = 0 }
109 if re_valid_kind("donor-to" as *u8) != 1 { ok = 0 }
110 if re_valid_kind("recruits" as *u8) != 1 { ok = 0 }
111 if re_valid_kind("hates" as *u8) != 0 { ok = 0 }
112 // contexts
113 if re_valid_context("personal" as *u8) != 1 { ok = 0 }
114 if re_valid_context("professional" as *u8) != 1 { ok = 0 }
115 if re_valid_context("church" as *u8) != 1 { ok = 0 }
116 if re_valid_context("civic" as *u8) != 1 { ok = 0 }
117 if re_valid_context("nonsense" as *u8) != 0 { ok = 0 }
118 // edge semantics (pos + neg)
119 if re_edge_ok("person" as *u8, "works-at" as *u8, "company" as *u8) != 1 { ok = 0 }
120 if re_edge_ok("person" as *u8, "works-at" as *u8, "org" as *u8) != 0 { ok = 0 }
121 if re_edge_ok("person" as *u8, "member-of" as *u8, "org" as *u8) != 1 { ok = 0 }
122 if re_edge_ok("person" as *u8, "member-of" as *u8, "household" as *u8) != 1 { ok = 0 }
123 if re_edge_ok("person" as *u8, "family" as *u8, "person" as *u8) != 1 { ok = 0 }
124 if re_edge_ok("company" as *u8, "family" as *u8, "person" as *u8) != 0 { ok = 0 }
125 if re_edge_ok("person" as *u8, "donor-to" as *u8, "org" as *u8) != 1 { ok = 0 }
126 // ---- the "relationships across everything" demo: ONE person, three worlds, one graph ----
127 p("-- one person (Rose), three contexts, ONE graph --\n" as *u8)
128 let e1: i64 = cmd_entity("andelinwest" as *u8, "person" as *u8, "Rose Andelin" as *u8)
129 let x1: i64 = cmd_edge("personal" as *u8, "person" as *u8, "Rose" as *u8, "family" as *u8, "person" as *u8, "Ken" as *u8, "personal" as *u8, "grandmother" as *u8)
130 let x2: i64 = cmd_edge("church" as *u8, "person" as *u8, "Rose" as *u8, "member-of" as *u8, "org" as *u8, "First Ward" as *u8, "church" as *u8, "Relief Society President" as *u8)
131 let x3: i64 = cmd_edge("andelinwest" as *u8, "person" as *u8, "Rose" as *u8, "works-at" as *u8, "company" as *u8, "Andelin West" as *u8, "professional" as *u8, "Managing Partner" as *u8)
132 // a genuinely invalid edge must be rejected (works-at a church org is not employment)
133 let x4: i64 = cmd_edge("andelinwest" as *u8, "person" as *u8, "Marcus" as *u8, "recruits" as *u8, "person" as *u8, "Emma" as *u8, "professional" as *u8, "tech recruiter" as *u8)
134 let xbad: i64 = cmd_edge("church" as *u8, "person" as *u8, "Bob" as *u8, "works-at" as *u8, "org" as *u8, "First Ward" as *u8, "church" as *u8, "-" as *u8)
135 let hh: i64 = cmd_entity("personal" as *u8, "household" as *u8, "Andelin Household" as *u8)
136 let cf: i64 = cmd_field("andelinwest" as *u8, "Rose" as *u8, "birthday" as *u8, "1950-03-14" as *u8)
137 if e1 != 0 { ok = 0 }
138 if x1 != 0 { ok = 0 }
139 if x2 != 0 { ok = 0 }
140 if x3 != 0 { ok = 0 }
141 if x4 != 0 { ok = 0 }
142 if xbad != 1 { ok = 0 }
143 if hh != 0 { ok = 0 }
144 if cf != 0 { ok = 0 }
145 p("NX-RELATE-SELFTEST types=4 kinds=7 contexts=5 cross-context-graph=1 " as *u8)
146 if ok == 1 { p("verdict=GREEN\n" as *u8); return 0 }
147 p("verdict=RED\n" as *u8)
148 return 1
149}
150
151func main(argc: i64, argv: *i64) -> i64 {
152 if argc < 2 { return cmd_selftest() }
153 let cmd: *u8 = argv[1] as *u8
154 if seq(cmd, "selftest" as *u8) == 1 { return cmd_selftest() }
155 if seq(cmd, "entity" as *u8) == 1 {
156 if argc < 5 { p("usage: entity <tenant> <type> <name>\n" as *u8); return 1 }
157 return cmd_entity(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8)
158 }
159 if seq(cmd, "edge" as *u8) == 1 {
160 if argc < 9 { p("usage: edge <tenant> <fromType> <fromName> <kind> <toType> <toName> <context> [role]\n" as *u8); return 1 }
161 var role: *u8 = "-" as *u8
162 if argc > 9 { role = argv[9] as *u8 }
163 return cmd_edge(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8, argv[7] as *u8, argv[8] as *u8, role)
164 }
165 if seq(cmd, "field" as *u8) == 1 {
166 if argc < 6 { p("usage: field <tenant> <entity> <key> <value>\n" as *u8); return 1 }
167 return cmd_field(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8)
168 }
169 p("NX-RELATE usage: entity | edge | field | selftest\n" as *u8)
170 return 1
171}