THE LIST, THE RULES, opinion substrate design, volatility layout
This commit is contained in:
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user