Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

gnomem.gno

10.38 Kb · 325 lines
  1// Package gnomem is a shared, contestable memory for multiple agents.
  2//
  3// Most "agent memory" products store text and retrieve similar chunks.
  4// That works for one agent talking to itself. It breaks the moment several
  5// independent agents — possibly run by different people, on different
  6// models — must maintain a *shared* understanding of the same world and
  7// sometimes disagree about it.
  8//
  9// gnomem models memory not as text but as a graph of structured claims.
 10// Each claim is a (subject, predicate, object) triple with an author,
 11// evidence and a confidence. Other agents can support it, contest it, add
 12// evidence, supersede it with a better claim, or trigger adjudication. No
 13// agent can silently erase an inconvenient finding: claims can be
 14// superseded or retracted, but the history stays visible and ordered.
 15//
 16// The point of the demo is the structure, not access control: anyone may
 17// write. Gating *who* may write to which claim is exactly what the
 18// capability-wallet demo adds on top.
 19package gnomem
 20
 21import (
 22	"chain"
 23	"chain/runtime"
 24	"strings"
 25
 26	"gno.land/p/nt/ufmt/v0"
 27)
 28
 29// Status is a claim's position in the argument lifecycle.
 30type Status string
 31
 32const (
 33	StatusProposed   Status = "proposed"   // asserted, no endorsements yet
 34	StatusSupported  Status = "supported"  // has support, no live contest
 35	StatusContested  Status = "contested"  // at least one live contest
 36	StatusAccepted   Status = "accepted"   // adjudicated true (terminal)
 37	StatusRetracted  Status = "retracted"  // adjudicated false / withdrawn (terminal)
 38	StatusSuperseded Status = "superseded" // replaced by a better claim (terminal)
 39)
 40
 41// Endorsement is a support or contest signal from one agent.
 42type Endorsement struct {
 43	Agent      address
 44	Confidence uint8 // 0..100
 45	Note       string
 46	Height     int64
 47}
 48
 49// Evidence points at off-chain material backing (or refuting) a claim.
 50type Evidence struct {
 51	By     address
 52	Kind   string // "report", "exploit", "dataset", "citation", ...
 53	Hash   string // commitment to the off-chain artifact
 54	Height int64
 55}
 56
 57// Claim is one node in the shared memory graph.
 58type Claim struct {
 59	ID           uint64
 60	Subject      string
 61	Predicate    string
 62	Object       string
 63	Author       address
 64	Confidence   uint8
 65	Status       Status
 66	CreatedAt    int64
 67	Supports     []Endorsement
 68	Contests     []Endorsement
 69	Evidence     []Evidence
 70	SupersededBy uint64 // 0 if none
 71	Supersedes   uint64 // 0 if none
 72	Resolver     address
 73	ResolvedAt   int64
 74}
 75
 76var claims []*Claim // index 0 => ID 1
 77
 78// ProposeClaim asserts a new (subject, predicate, object) triple.
 79func ProposeClaim(cur realm, subject, predicate, object, evidenceHash string, confidence uint8) uint64 {
 80	assert(subject != "" && predicate != "" && object != "", "empty triple field")
 81	assertConfidence(confidence)
 82
 83	id := uint64(len(claims)) + 1
 84	c := &Claim{
 85		ID:         id,
 86		Subject:    subject,
 87		Predicate:  predicate,
 88		Object:     object,
 89		Author:     cur.Previous().Address(),
 90		Confidence: confidence,
 91		Status:     StatusProposed,
 92		CreatedAt:  runtime.ChainHeight(),
 93	}
 94	if evidenceHash != "" {
 95		c.Evidence = append(c.Evidence, Evidence{
 96			By: c.Author, Kind: "initial", Hash: evidenceHash, Height: c.CreatedAt,
 97		})
 98	}
 99	claims = append(claims, c)
100	chain.Emit("ClaimProposed", "id", ufmt.Sprintf("%d", id), "subject", subject, "predicate", predicate)
101	return id
102}
103
104// SupportClaim endorses a claim. Support never overrides a live contest —
105// a contested claim stays contested until adjudicated.
106func SupportClaim(cur realm, id uint64, confidence uint8, note string) {
107	c := mustOpen(id)
108	assertConfidence(confidence)
109	c.Supports = append(c.Supports, Endorsement{
110		Agent: cur.Previous().Address(), Confidence: confidence, Note: note, Height: runtime.ChainHeight(),
111	})
112	if c.Status == StatusProposed {
113		c.Status = StatusSupported
114	}
115	chain.Emit("ClaimSupported", "id", ufmt.Sprintf("%d", id))
116}
117
118// ContestClaim challenges a claim, optionally attaching refuting evidence.
119func ContestClaim(cur realm, id uint64, note, evidenceHash string) {
120	c := mustOpen(id)
121	agent := cur.Previous().Address()
122	h := runtime.ChainHeight()
123	c.Contests = append(c.Contests, Endorsement{
124		Agent: agent, Confidence: 0, Note: note, Height: h,
125	})
126	if evidenceHash != "" {
127		c.Evidence = append(c.Evidence, Evidence{By: agent, Kind: "refutation", Hash: evidenceHash, Height: h})
128	}
129	c.Status = StatusContested
130	chain.Emit("ClaimContested", "id", ufmt.Sprintf("%d", id))
131}
132
133// AddEvidence attaches supporting or contextual off-chain material.
134func AddEvidence(cur realm, id uint64, kind, hash string) {
135	c := mustOpen(id)
136	assert(hash != "", "empty evidence hash")
137	c.Evidence = append(c.Evidence, Evidence{
138		By: cur.Previous().Address(), Kind: kind, Hash: hash, Height: runtime.ChainHeight(),
139	})
140}
141
142// SupersedeClaim replaces an open claim with a new one. The old claim is
143// marked superseded (terminal) and linked to its replacement, so the
144// correction is visible rather than a silent overwrite.
145func SupersedeClaim(cur realm, oldID uint64, subject, predicate, object, evidenceHash string, confidence uint8) uint64 {
146	old := mustOpen(oldID)
147	newID := ProposeClaim(cur, subject, predicate, object, evidenceHash, confidence)
148	newC := claims[newID-1]
149	newC.Supersedes = oldID
150	old.Status = StatusSuperseded
151	old.SupersededBy = newID
152	chain.Emit("ClaimSuperseded", "old", ufmt.Sprintf("%d", oldID), "new", ufmt.Sprintf("%d", newID))
153	return newID
154}
155
156// ResolveClaim adjudicates an open claim as accepted (true) or retracted
157// (false/withdrawn). The resolver's address is recorded — resolution is
158// itself a provenance-bearing act, not an anonymous verdict.
159func ResolveClaim(cur realm, id uint64, accepted bool) {
160	c := mustOpen(id)
161	if accepted {
162		c.Status = StatusAccepted
163	} else {
164		c.Status = StatusRetracted
165	}
166	c.Resolver = cur.Previous().Address()
167	c.ResolvedAt = runtime.ChainHeight()
168	chain.Emit("ClaimResolved", "id", ufmt.Sprintf("%d", id), "status", string(c.Status))
169}
170
171// ---- read-only API ----
172
173// Get returns a copy of a claim by id.
174func Get(id uint64) Claim { return *mustGet(id) }
175
176// Count returns the number of claims in the graph.
177func Count() int { return len(claims) }
178
179// IsOpen reports whether a claim can still be supported/contested/resolved.
180func IsOpen(id uint64) bool { return isOpen(mustGet(id).Status) }
181
182// ---- internal ----
183
184func isOpen(s Status) bool {
185	return s == StatusProposed || s == StatusSupported || s == StatusContested
186}
187
188func mustGet(id uint64) *Claim {
189	assert(id >= 1 && id <= uint64(len(claims)), "unknown claim")
190	return claims[id-1]
191}
192
193func mustOpen(id uint64) *Claim {
194	c := mustGet(id)
195	assert(isOpen(c.Status), "claim is resolved (terminal): "+string(c.Status))
196	return c
197}
198
199func assertConfidence(c uint8) { assert(c <= 100, "confidence must be 0..100") }
200
201func assert(cond bool, msg string) {
202	if !cond {
203		panic(msg)
204	}
205}
206
207// Render shows the full graph, or one claim's argument tree at :<id>.
208func Render(path string) string {
209	if path == "" {
210		return renderIndex()
211	}
212	return renderClaim(parseID(path))
213}
214
215func renderIndex() string {
216	var sb strings.Builder
217	sb.WriteString("# GnoMem — Contested Shared Memory\n\n")
218	sb.WriteString(ufmt.Sprintf("A graph of %d structured claim(s) maintained by multiple agents.\n\n", len(claims)))
219	if len(claims) == 0 {
220		sb.WriteString("_No claims yet._\n")
221		return sb.String()
222	}
223	sb.WriteString("| # | Claim | Status | ✋ support | ⚔ contest |\n")
224	sb.WriteString("|---|---|---|---|---|\n")
225	for _, c := range claims {
226		sb.WriteString(ufmt.Sprintf("| [%d](/r/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/gnomem:%d) | %s | %s | %d | %d |\n",
227			c.ID, c.ID, triple(c), statusBadge(c.Status), len(c.Supports), len(c.Contests)))
228	}
229	return sb.String()
230}
231
232func renderClaim(id uint64) string {
233	c := mustGet(id)
234	var sb strings.Builder
235	sb.WriteString(ufmt.Sprintf("# Claim #%d\n\n", c.ID))
236	sb.WriteString(ufmt.Sprintf("> **%s**\n\n", triple(c)))
237	sb.WriteString(ufmt.Sprintf("- **Status:** %s\n", statusBadge(c.Status)))
238	sb.WriteString(ufmt.Sprintf("- **Author:** %s\n", short(c.Author)))
239	sb.WriteString(ufmt.Sprintf("- **Author confidence:** %d/100\n", c.Confidence))
240	sb.WriteString(ufmt.Sprintf("- **Created at height:** %d\n", c.CreatedAt))
241	if c.Supersedes != 0 {
242		sb.WriteString(ufmt.Sprintf("- **Supersedes:** [#%d](/r/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/gnomem:%d)\n", c.Supersedes, c.Supersedes))
243	}
244	if c.SupersededBy != 0 {
245		sb.WriteString(ufmt.Sprintf("- **Superseded by:** [#%d](/r/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/gnomem:%d)\n", c.SupersededBy, c.SupersededBy))
246	}
247	if c.ResolvedAt != 0 {
248		sb.WriteString(ufmt.Sprintf("- **Resolved by:** %s at height %d\n", short(c.Resolver), c.ResolvedAt))
249	}
250	sb.WriteString("\n")
251
252	sb.WriteString(ufmt.Sprintf("## ✋ Support (%d)\n\n", len(c.Supports)))
253	renderEndorsements(&sb, c.Supports)
254	sb.WriteString(ufmt.Sprintf("## ⚔ Contest (%d)\n\n", len(c.Contests)))
255	renderEndorsements(&sb, c.Contests)
256
257	sb.WriteString(ufmt.Sprintf("## 📎 Evidence (%d)\n\n", len(c.Evidence)))
258	if len(c.Evidence) == 0 {
259		sb.WriteString("_None._\n")
260	} else {
261		sb.WriteString("| By | Kind | Commitment | Height |\n|---|---|---|---|\n")
262		for _, e := range c.Evidence {
263			sb.WriteString(ufmt.Sprintf("| %s | %s | `%s` | %d |\n", short(e.By), e.Kind, shortHash(e.Hash), e.Height))
264		}
265	}
266	return sb.String()
267}
268
269func renderEndorsements(sb *strings.Builder, es []Endorsement) {
270	if len(es) == 0 {
271		sb.WriteString("_None._\n\n")
272		return
273	}
274	sb.WriteString("| Agent | Confidence | Note | Height |\n|---|---|---|---|\n")
275	for _, e := range es {
276		sb.WriteString(ufmt.Sprintf("| %s | %d | %s | %d |\n", short(e.Agent), e.Confidence, e.Note, e.Height))
277	}
278	sb.WriteString("\n")
279}
280
281func triple(c *Claim) string {
282	return ufmt.Sprintf("%s — %s — %s", c.Subject, c.Predicate, c.Object)
283}
284
285func statusBadge(s Status) string {
286	switch s {
287	case StatusAccepted:
288		return "✅ accepted"
289	case StatusRetracted:
290		return "🚫 retracted"
291	case StatusSuperseded:
292		return "♻️ superseded"
293	case StatusContested:
294		return "⚔ contested"
295	case StatusSupported:
296		return "✋ supported"
297	default:
298		return "· proposed"
299	}
300}
301
302func parseID(s string) uint64 {
303	var n uint64
304	for i := 0; i < len(s); i++ {
305		c := s[i]
306		assert(c >= '0' && c <= '9', "invalid claim path: "+s)
307		n = n*10 + uint64(c-'0')
308	}
309	return n
310}
311
312func short(a address) string {
313	s := a.String()
314	if len(s) <= 12 {
315		return s
316	}
317	return s[:8] + "…" + s[len(s)-4:]
318}
319
320func shortHash(s string) string {
321	if len(s) <= 12 {
322		return s
323	}
324	return s[:6] + "…" + s[len(s)-4:]
325}