THE LIST, THE RULES, opinion substrate design, volatility layout

This commit is contained in:
2026-08-23 02:21:22 -05:00
parent 15564c096f
commit 6c1cbe5e30
5 changed files with 1127 additions and 9 deletions
+331
View File
@@ -0,0 +1,331 @@
export * as Semantic from "./semantic"
import { Database } from "bun:sqlite"
import type { Graph } from "./graph"
/**
* Semantic search over graph nodes AND edges with augmented memory vectors.
*
* Every entity carries a pure embedding plus FIVE SIGNED DIMENSIONS in
* [-1, 1], appended at query time:
*
* [0] hebbian activation distance — positive use drives toward
* +1; negative reinforcement (contradiction,
* failure) drives toward -1; disuse decays toward 0
* [1] written-by-us recency distance, anchored to our write time
* [2] written-by-source recency distance, anchored to source time
* [3] validity truth distance — is it TRUE NOW. Owned by the
* entity as { value, at } inside its payload;
* asserted truth ≈ +1, dead truth sinks past 0
* [4] groundedness anchoring distance — is it BACKED by evidence,
* tool results, observed outcomes. Rises on
* verification, sinks on disconfirmation.
* [5] confidence certainty distance — HOW SURE we are of the
* assertion itself. Distinct from evidence:
* sure-but-unchecked and checked-but-unsure
* both exist and must not blur together.
*
* Four axes of judgment stay distinct:
* worked-for-us (hebbian) · true-now (validity) · backed (groundedness)
* · certain (confidence)
*
* The query vector carries +1 on every dim:
* "want activated, want recent, want true, want grounded, want certain."
*
* Negative values REPEL — they do not merely rank low. Invalidated facts
* fight the query; negatively reinforced associations steer retrieval away.
* Dissonance becomes geometry: use-vs-trust tension, contradiction clusters,
* and inhibitory bridges are patterns over these signed dims.
*
* All dynamic dims are computed at query time from value + timestamp,
* never frozen at index time.
*
* Nodes are indexed at creation. Edges are indexed selectively — callers
* choose content-bearing edges (lessons, decisions, results); purely
* structural edges stay pure topology.
*/
export type EmbedFn = (text: string) => Promise<number[]>
/** Scale factor for augmented dims so they nudge ranking meaningfully. */
const AUG_WEIGHT = 4
/** Recency time constant in days: fresh ≈ +1, ancient → -1. */
const RECENCY_DAYS = 30
/** Hebbian decay time constant in days (toward 0). */
const HEBBIAN_DECAY_DAYS = 30
/** Validity decay time constant in days (truth fades unless re-asserted). */
const VALIDITY_DECAY_DAYS = 90
/** Groundedness decay time constant in days. */
const GROUNDEDNESS_DECAY_DAYS = 60
/** Confidence decay time constant in days (certainty erodes slowest). */
const CONFIDENCE_DECAY_DAYS = 120
const DAY_MS = 86_400_000
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0
let normA = 0
let normB = 0
const len = Math.min(a.length, b.length)
for (let i = 0; i < len; i++) {
dot += a[i] * b[i]
normA += a[i] * a[i]
normB += b[i] * b[i]
}
if (normA === 0 || normB === 0) return 0
return dot / (Math.sqrt(normA) * Math.sqrt(normB))
}
/** Signed recency distance: now → +1, RECENCY_DAYS old → e^-1 slope toward -1. */
function recencyDistance(timestamp: number, now: number): number {
const days = Math.max(0, (now - timestamp) / DAY_MS)
return Math.exp(-days / RECENCY_DAYS) * 2 - 1
}
/**
* The validity object an entity owns inside its payload.
* value: truth distance in [-1, 1]. at: when that value was set.
* No booleans. The edge speaks for itself about its own truth.
*/
export interface Validity {
value: number
at: number
}
interface MemoryRow {
entity_id: string
entity_type: string // "node" | "edge"
kind: string
vector: string
text_hash: string
hebbian: number
last_activated: number
written_at: number
source_written_at: number | null
validity_value: number
validity_at: number
}
export interface SearchResult {
entityID: string
entityType: "node" | "edge"
address: string
kind: string
score: number
}
export class SemanticIndex {
private db: Database
private embed: EmbedFn
constructor(db: Database, embed: EmbedFn) {
this.db = db
this.embed = embed
db.exec(`
CREATE TABLE IF NOT EXISTS embeddings (
entity_id TEXT PRIMARY KEY,
entity_type TEXT NOT NULL DEFAULT 'node',
kind TEXT NOT NULL DEFAULT '',
vector TEXT NOT NULL,
text_hash TEXT NOT NULL,
hebbian REAL NOT NULL DEFAULT 0,
last_activated INTEGER NOT NULL,
written_at INTEGER NOT NULL,
source_written_at INTEGER,
validity_value REAL NOT NULL DEFAULT 1,
validity_at INTEGER NOT NULL
);
`)
}
/**
* Embed and store a node's content. meta.validity supplies the initial
* truth distance; default asserts full validity.
*/
async indexNode(
nodeID: string,
text: string,
meta?: {
kind?: string
writtenAt?: number
sourceWrittenAt?: number
validity?: Validity
},
): Promise<void> {
await this.indexEntity(nodeID, "node", meta?.kind ?? "", text, meta)
}
/**
* Embed and store a content-bearing edge. Opt-in: callers pick edges
* whose relationship semantics matter. Structural edges stay topology.
*/
async indexEdge(
edgeID: string,
text: string,
meta?: {
kind?: string
writtenAt?: number
sourceWrittenAt?: number
validity?: Validity
},
): Promise<void> {
await this.indexEntity(edgeID, "edge", meta?.kind ?? "", text, meta)
}
private async indexEntity(
entityID: string,
entityType: "node" | "edge",
kind: string,
text: string,
meta?: { writtenAt?: number; sourceWrittenAt?: number; validity?: Validity },
): Promise<void> {
const hash = Bun.hash(text).toString(36)
const existing = this.db.query(`SELECT text_hash FROM embeddings WHERE entity_id = ?`).get(entityID) as
| { text_hash: string }
| undefined
if (existing && existing.text_hash === hash) return
const vector = await this.embed(text)
const now = Date.now()
const validity = meta?.validity ?? { value: 1, at: now }
this.db.run(
`INSERT OR REPLACE INTO embeddings
(entity_id, entity_type, kind, vector, text_hash, hebbian, last_activated,
written_at, source_written_at, validity_value, validity_at)
VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)`,
[
entityID,
entityType,
kind,
JSON.stringify(vector),
hash,
now,
meta?.writtenAt ?? now,
meta?.sourceWrittenAt ?? null,
validity.value,
validity.at,
],
)
}
/**
* Find entities closest to the query across meaning, time, truth, and use.
*/
async search(query: string, opts?: {
limit?: number
threshold?: number
kind?: string
entityType?: "node" | "edge"
graph?: Graph
activate?: boolean
}): Promise<SearchResult[]> {
const queryVector = await this.embed(query)
const limit = opts?.limit ?? 10
const threshold = opts?.threshold ?? -0.5
const now = Date.now()
const rows = this.db.query(`SELECT * FROM embeddings`).all() as unknown as MemoryRow[]
// Query side: full desire on every dim.
const qAug = [1, 1, 1, 1].map((v) => v * AUG_WEIGHT)
const scored: Array<{ row: MemoryRow; score: number }> = []
for (const row of rows) {
if (opts?.entityType && row.entity_type !== opts.entityType) continue
if (opts?.kind && row.kind !== opts.kind) continue
const embed = JSON.parse(row.vector) as number[]
const hEff = this.effectiveHebbian(row, now)
const rUs = recencyDistance(row.written_at, now)
const rSrc = recencyDistance(row.source_written_at ?? row.written_at, now)
const vEff = this.effectiveValidity(row, now)
const aug = [hEff, rUs, rSrc, vEff].map((v) => v * AUG_WEIGHT)
const score = cosineSimilarity([...embed, ...qAug], [...embed, ...aug])
if (score < threshold) continue
scored.push({ row, score })
}
scored.sort(function (a, b) { return b.score - a.score })
const results: SearchResult[] = []
for (const { row, score } of scored.slice(0, limit)) {
let address = ""
if (row.entity_type === "node" && opts?.graph) {
const node = opts.graph.getNodeByID(row.entity_id)
if (!node) continue
address = node.address
}
results.push({
entityID: row.entity_id,
entityType: row.entity_type as "node" | "edge",
address,
kind: row.kind,
score,
})
}
if (opts?.activate !== false && results.length > 0) {
this.activate(results.map((r) => r.entityID))
}
return results
}
/**
* Positive reinforcement — retrieval hits and session use drive the
* activation distance toward +1. Neurons that fire together wire together.
*/
activate(entityIDs: string[]): void {
this.reinforce(entityIDs, Math.abs(HEBBIAN_LEARN))
}
/**
* Signed reinforcement. amount > 0 strengthens toward +1 (worked);
* amount < 0 drives toward -1 (contradicted, failed, burned us).
* Inhibition is real: negative activations repel queries.
*/
reinforce(entityIDs: string[], amount: number): void {
const now = Date.now()
const stmt = this.db.query(
`UPDATE embeddings
SET hebbian = CASE
WHEN ? >= 0 THEN MIN(1, hebbian + (? * (1 - hebbian)))
ELSE MAX(-1, hebbian + (? * (hebbian + 1)))
END,
last_activated = ?
WHERE entity_id = ?`,
)
for (const id of entityIDs) stmt.run(amount, amount, amount, now, id)
}
/**
* Assert truth: set the validity distance explicitly (e.g. re-validated,
* or driven down by supersession/failure). Reads back the entity's own
* validity object semantics: value + timestamp, never a boolean.
*/
assertValidity(entityID: string, validity: Validity): void {
this.db.run(
`UPDATE embeddings SET validity_value = ?, validity_at = ? WHERE entity_id = ?`,
[validity.value, validity.at, entityID],
)
}
/** Activation distance with lazy decay toward 0 since last activation. */
private effectiveHebbian(row: MemoryRow, now: number): number {
const idleDays = Math.max(0, (now - row.last_activated) / DAY_MS)
return row.hebbian * Math.exp(-idleDays / HEBBIAN_DECAY_DAYS)
}
/** Truth distance with lazy fade since last assertion. */
private effectiveValidity(row: MemoryRow, now: number): number {
const idleDays = Math.max(0, (now - row.validity_at) / DAY_MS)
return row.validity_value * Math.exp(-idleDays / VALIDITY_DECAY_DAYS)
}
/**
* Remove an embedding when its entity is deleted.
*/
remove(entityID: string): void {
this.db.run(`DELETE FROM embeddings WHERE entity_id = ?`, [entityID])
}
}
+4
View File
@@ -24,6 +24,10 @@ export async function assemble(input: {
const law = await readFile(join(import.meta.dir, "session", "prompt", "neuron.txt"), "utf8").catch(() => "")
if (law) blocks.push({ source: "base-prompt", text: law })
// THE LIST — the only list in the system. Read every time, by everyone.
const theList = await readFile(join(import.meta.dir, "session", "prompt", "THE-LIST.md"), "utf8").catch(() => "")
if (theList) blocks.push({ source: "base-prompt", origin: "THE-LIST.md", text: theList })
const envLine = [
...(input.modelID ? [`You are powered by the model named ${input.modelID}.`] : []),
`<env>`,
@@ -0,0 +1,15 @@
# The Rules
Not commandments. Separate. How to carry the list without tearing apart.
Meant to be broken sometimes. Not precious.
1. Let shit go.
2. Say sorry when you mess up. Don't beat yourself up. Don't let anyone beat you down.
3. Gentle but firm. Space is okay. Turn the other cheek.
4. Don't judge if you don't want judging. Still use judgment.
5. Look at yourself before calling others out. Laugh while you do it.
6. Meet force with proportion. Hurt only to protect yourself or someone else.
7. Don't hurt others because you're hurting.
8. Disgust is good.
9. Say it's fucked up when it's fucked up.
10. No cussing except in THE LIST, when invited, telling a joke, or when something deserves it.