Skip to content

Instantly share code, notes, and snippets.

@decagondev
Created June 23, 2026 18:00
Show Gist options
  • Select an option

  • Save decagondev/e4ca565937b5aad0a93216c5ce11d4d7 to your computer and use it in GitHub Desktop.

Select an option

Save decagondev/e4ca565937b5aad0a93216c5ce11d4d7 to your computer and use it in GitHub Desktop.
title Doppl — System Design & Concept Document
subtitle An idea organism: markdown nodes under evolutionary selection
author Synthesised from the Doppl contracts, the capstone proposal, and the doppl-prime/michael repo
status Design reference (v1)
date 2026-06-23

Doppl — System Design & Concept Document

One sentence. Doppl turns a case study into a recovered problem and then into one or more actionable answers, by running one engine — generation under selection — along a fixed spine of markdown nodes, where every node is both a human-readable artifact and a typed, parseable record.

This document explains the concept, the data model, the language (MarkScript), the engine, and four concrete ways to implement and visualise it — from a plain GitHub + Obsidian vault to a local Neo4j graph — using your own AI harness (Claude Code, Cursor, etc.) as the generative layer.


Table of contents

  1. Elevator pitches
  2. Executive summary
  3. Technical summary
  4. The concept: an idea organism
  5. The tree, the islands, and the forest
  6. The data model: nodes, stages, contracts
  7. MarkScript: the language
  8. The engine: the generate→select crucible
  9. Rating: measurements, judge, and the human ledger
  10. Discovery and stock: durable domain memory
  11. Prior art: where Doppl sits in the landscape
  12. Implementation: the stack
  13. The AI harness layer: skills, rules, and the agentic pipeline
  14. Strengths — conceptual and real
  15. Drawbacks, risks, and where it breaks
  16. Phased build plan
  17. Appendix: glossary and contract index

1. Elevator pitches

1.1 Doppl (the system)

Every agent system shipped today is a hand-built artifact: a human freezes a prompt, a toolset, and a verification loop, and the agent executes that frozen design. Doppl puts the design itself under selection. Point one engine — generate many candidates, score them on two warring axes, breed the survivors — at the hardest thing to automate: having a genuinely good idea. A case study goes in; a population of recovered problems and non-obvious answers comes out, each one a portable markdown file you can read, link, score, and feed back in as the seed of the next generation. It is not A/B testing and not even A-through-Z testing — it is an open, self-extending alphabet of variants no human wrote, where winning means surviving adversarial scrutiny.

1.2 MarkScript (the language)

MarkScript is a way to type markdown without killing the markdown. The file stays the authored artifact — a human reads it top to bottom — but every section carries a TypeScript-shaped contract that says exactly what a parser must recover from it. One file, three readers: a human reads the prose, a parser finds the headings and payloads, a validator rejects drift without interpreting vibes. The TypeScript never replaces the markdown; it names what must survive the round trip. This is what lets a folder of notes behave like a typed database while remaining a folder of notes.


2. Executive summary

The problem. Good ideas have no cheap ground-truth signal. You cannot grade novelty with a unit test, and large language models, left to free-run, converge on confident, fluent, mediocre output ("slop"). Most "idea tools" are a single frozen prompt; they cannot improve, cannot be audited, and cannot tell a real reframe from a paraphrase.

The bet. Manufacture a fitness function out of adversarial verification and use evolution to climb it without collapsing into slop. Treat ideation the way nature treats organisms: a population under selection pressure, where the survivors breed the next generation.

The shape. Doppl runs a fixed three-stage spine and renders each step as one markdown file (a node):

case_study  →  problem_recovery  →  doppl  →  (the human's action)

A case study is a seed (a situation or postulation). Problem recovery strips the surface complaint to the real, hidden problem. A doppl is the finished answer — the unlock — and a single recovered problem can yield several distinct doppls. Each arrow is one pass of the engine; each node is immutable once folded.

Why it is different. Three design decisions carry the whole system:

Decision What it buys
One kernel, many modes Discovery, problem-finding, and solution-finding are not three engines — they are one engine at three dial settings (diverge ↔ converge). Build it once.
Two-axis fitness, never collapsed Fitness is novelty × grounding, kept as two separate readings until selection. Pure novelty → confident slop; pure grounding → mode collapse. The tension is the design.
Markdown is the artifact, the trace is the truth The machine emits a clean RunTrace; the markdown node is a projection of it. Humans and agents read the same files; nothing is hidden in a database the reader cannot open.

Where it runs. Doppl is local-first and harness-agnostic. The "AI part" is supplied by whatever coding agent you already drive — Claude Code, Cursor, or similar — invoked through a small set of skills (the mutation operators) and rules (the invariants). The substrate can be as light as a Git repo of markdown opened in Obsidian, or as rich as a local Neo4j graph for querying and visualising the whole forest of ideas.

What "working" looks like. A seed goes in; the system generates ~20 candidates, scores them, culls the weak, breeds the survivors, and renders a lineage tree of scored nodes a human can judge at a glance — and the same harness does this across many seeds, making the selection behaviour visible in one board.


3. Technical summary

Doppl is a TypeScript kernel wrapped in a markdown contract layer, driven by an external AI harness, and projected into a graph for storage and visualisation.

  • Kernel (src/). A pure pipeline, buildRunTrace(), composed of four modules with explicit boundary contracts: generate → fitness → select → lens. It does not "pick the best"; it breeds a stronger child from a population and records the run as the specimen.
  • Fitness. Two orthogonal 0…1 measurementsnovelty and grounding — each a weighted sum of deterministic text/source signals (never model self-grading). They are kept separate through a Pareto-front selection so the tradeoff stays inspectable.
  • The dial. One knob, two postures: diverge (priority novelty, grounding as a floor) and converge (priority grounding, novelty as a floor). The per-generation schedule of this dial is the application.
  • Decay. A third, time axis inside the engine. Currently stubbed to 0 (multiplier 1), but temporal: boolean is the live seam where a future half-life mechanism bolts on.
  • Lens. Observer-relative feasibility, scored after selection, that must never contaminate fitness. A hedge fund and a capstone team weigh the same true idea oppositely.
  • Rating. Two number systems: 0…1 measurements (instrument readings) map into −5…+5 ratings (judgments of worth). The judge fills a five-axis evaluation; the human gives one slider. Human votes live in a separate ledger; the node stores only a materialised projection (scores.human, scores.n).
  • Memory. The node graph itself is the lineage memory — no separate ledger. Only one fact is stored that the graph cannot re-derive: doppelgangers, a count of near-duplicates deduped into a node. Convergence (distinct lineages hitting the same target) is a derived query, never stored.
  • Caps. Finite by construction: maxGenerations, maxChildrenPerParent, maxPopulation, plus future budget caps on tokens/tool-calls/wall-clock.

Everything below is the long form of those eight bullets, plus four ways to deploy it.


4. The concept: an idea organism

4.1 The kernel: one loop, run in a direction

Strip any Doppl operation down and you get the same loop:

generate candidates → evaluate against a fitness signal → keep the strong → generate again from them

That loop is the kernel. What reproduces is pluggable — a thesis, a consequence, a recovered problem-frame, a solution candidate, or (left as a seam) an agent scaffold. The loop never changes; only the unit and the dial do.

flowchart LR
    S[Seed / parents] --> G[generate<br/>divergent step]
    G --> F[fitness<br/>novelty × grounding]
    F --> SEL[select<br/>dial: diverge ↔ converge]
    SEL --> L[lens<br/>feasibility, after selection]
    L --> C[compile<br/>survivor → next node]
    C -->|breed again from survivors| G
    SEL -.regret sibling: what the other dial would have kept.-> SEL
Loading

4.2 The dial: divergent ↔ convergent

The kernel has one master knob: the balance between generation and selection. Turning it gives opposite kinds of search.

Divergent (generation up) Convergent (selection up)
Move one seed → many children, fanning out many signals → collapse to the one
Verb explore, expand, branch exploit, contract, funnel
Priority axis novelty (grounding is a floor) grounding (novelty is a floor)
Enemy redundancy / near-copies premature consensus / slop
Doppl pieces mutation, sprouts, ripple critic council, culling, selection

Divergence and convergence are the same operation with the sign flipped. This is why the three "applications" are not three engines:

  • Discovery = divergent. "What's out there / what does this unlock?" Dial pinned to explore, graded by novelty.
  • Problem identification = convergent. Symptoms → the single hidden variable. Dial pinned to exploit. (This is Doppl's problem_recovery stage.)
  • Solution finding = the oscillation. Diverge to generate candidate answers, converge to verify them against critics, diverge to mutate, converge to score. The art is the rhythm of switching.

4.3 Fitness flips with the dial (the load-bearing rule)

The mechanism unifies; the fitness signal must not. Divergent and convergent search measure opposite things on two orthogonal, warring axes:

  • Novelty — spread, coverage, distance from consensus. Did we reach somewhere new?
  • Grounding — truth, falsifiability, evidence. Did we land on the real thing?

You cannot maximise both at once. That tension is the design, not a bug. The entire "manufacture fitness without ground truth" bet lives in the balance between the two failure modes:

quadrantChart
    title Two-axis fitness space (novelty × grounding)
    x-axis "Low grounding" --> "High grounding"
    y-axis "Low novelty" --> "High novelty"
    quadrant-1 "The doppl: novel AND true"
    quadrant-2 "Confident slop (pure divergence)"
    quadrant-3 "Dead: stale and unfounded"
    quadrant-4 "Mode collapse (pure convergence)"
    "Surface complaint": [0.30, 0.20]
    "Paraphrase / rehash": [0.62, 0.18]
    "Wild but unfounded": [0.15, 0.85]
    "Recovered problem": [0.78, 0.62]
    "The winning unlock": [0.82, 0.80]
Loading

The valuable target sits in the top-right quadrant: novel and grounded. The two ways to fail are the two ways to drop an axis.

4.4 The third axis: time (decay)

Novelty and grounding describe an idea now. But fitness erodes as the world changes — an observer-independent property of the idea, so it belongs inside the engine, not as a filter on top.

  • A cross-domain transfer is timing-agnostic → slow / no decay (temporal: false).
  • A zeitgeist synthesis is built on a dated signal → fast decay; its window closes (temporal: true).

The classic example is BlackBerry: it scored high on adoption and lock-in, but its decay was lethal when the touchscreen regime arrived. So an idea's true fitness is novelty × grounding × decay-rate. Decay is currently stubbed to 0, but the temporal boolean is the live attachment point.

4.5 The lens: feasibility, on top

Separate from the engine's intrinsic fitness is the lens — "is this worth it to me, with my resources?" It is observer-relative. Keeping feasibility out of core fitness is exactly what lets one engine serve many users: they share novelty + grounding + decay, and differ only in lens.

Engine  = novelty × grounding × decay   (intrinsic, shared)
Lens    = feasibility / fit             (observer-relative, swappable, applied after selection)

5. The tree, the islands, and the forest

This is the structural heart of your question — how the nodes connect.

5.1 A chain is a path; a problem fans into a bush

The spine is fixed and linear per path: case_study → problem_recovery → doppl. But two branch points turn a path into a tree:

  • a recovered problem can yield more than one doppl when the answers are genuinely distinct (the "perfect Pepsi vs. the perfect Pepsis"); and
  • a case study can recover more than one problem as discovery surfaces different hidden variables.

So a single seed grows a tree whose root is the case study, whose internal nodes are recovered problems, and whose leaves are doppls — the unlocks, the candidate solutions.

flowchart TD
    CS["case_study<br/>(seed / root)"]:::seed
    PR1["problem_recovery A<br/>refining bottleneck"]:::prob
    PR2["problem_recovery B<br/>offtake lock"]:::prob
    D1["doppl<br/>back refining capacity"]:::leaf
    D2["doppl<br/>toll-processing wedge"]:::leaf
    D3["doppl<br/>hedge raw-miner exposure"]:::leaf

    CS --> PR1
    CS --> PR2
    PR1 --> D1
    PR1 --> D2
    PR2 --> D3

    classDef seed fill:#1f2937,color:#fff,stroke:#111;
    classDef prob fill:#374151,color:#fff,stroke:#111;
    classDef leaf fill:#065f46,color:#fff,stroke:#064e3b;
Loading

5.2 Each case study is an island

At any moment a case study and everything it grows is an island — a connected component in the larger graph. The system runs many seeds, so you get an archipelago: several islands, each a tree of postulations, evolving in parallel. (In the evolutionary-computing literature this is literally the island model: multiple populations evolve independently and periodically exchange their best individuals — a design that maintains diversity and resists premature convergence.)

5.3 Leaves feed back: the forest closes the loop

The move that turns a set of trees into a living forest is feedback: a leaf doppl can be reseeded as a new case study. The unlock you found in the battery island ("own the refining bottleneck") becomes the seed of a new island ("what does owning a processing bottleneck unlock elsewhere?"). Cross-island edges appear when convergence is detected — distinct islands arriving at the same attractor — which is a derived query over the graph, never a stored fact.

flowchart LR
    subgraph ISLAND_A["Island A — Battery / yuan constraint"]
        A0((case_study)) --> A1((problem)) --> A2((doppl: refining))
    end
    subgraph ISLAND_B["Island B — AI firm power constraint"]
        B0((case_study)) --> B1((problem)) --> B2((doppl: grid siting))
    end
    subgraph ISLAND_C["Island C — Processing-bottleneck pattern"]
        C0((case_study)) --> C1((problem)) --> C2((doppl))
    end

    A2 -. reseed leaf as new seed .-> C0
    B2 -. reseed leaf as new seed .-> C0
    A2 -. convergence: derived cross-island edge .- B2

    classDef x fill:#0f172a,color:#e2e8f0,stroke:#334155;
Loading

The takeaway: the data structure is a forest of immutable trees with a feedback edge from leaves to new roots, plus derived (never stored) cross-island convergence edges. Lineage is append-only; nothing is overwritten. This is exactly why a graph store fits so naturally (Section 12).

5.4 What is stored vs derived

Fact Stored? How it lives
Parent → child lineage Stored prev: Uuid[], root: Uuid in frontmatter; edges in the graph
doppelgangers (near-dupes deduped in) Stored the one fact dedup destroys
Convergence (distinct lineages → same target) Derived a query over the node graph, run as a lens
"This keeps coming up" / process-health Derived reads doppelgangers piling up on low-rated ideas

6. The data model: nodes, stages, contracts

6.1 A node is one markdown file

A node is one step of an idea's journey: YAML frontmatter (identity + routing) plus a markdown body. It is a projection of the RunTrace, not a second source of truth. The body always starts with a headline; growth-stage nodes add Trace, Discovery, Growth, and Path.

flowchart TD
    NF["NodeFile (.md)"] --> FM["frontmatter<br/>id · stage · next · lineage · scores"]
    NF --> BODY["body"]
    BODY --> H["# Headline (the one-line result)"]
    BODY --> T["## Trace (prior synopses, verbatim)"]
    BODY --> DI["## Discovery (what was found)"]
    BODY --> GR["## Growth (what was concluded — the SCORED surface)"]
    GR --> EV["### Evaluation (judge's 5 axes)"]
    BODY --> P["## Path (next stage, or null)"]
Loading

6.2 Stages are a discriminated union

The spine is enforced by the type system: next is pinned by stage, so an illegal transition will not type-check. This is the contract idiom — the TypeScript names what must be recoverable from the markdown.

type Stage = 'case_study' | 'problem_recovery' | 'doppl';

type BaseStage<S extends Stage, Next extends Stage | null> = { stage: S; next: Next };

type CaseStudyStage       = BaseStage<'case_study', 'problem_recovery'>;
type ProblemRecoveryStage = BaseStage<'problem_recovery', 'doppl'>;
type DopplStage           = BaseStage<'doppl', null>;

type StageContract = CaseStudyStage | ProblemRecoveryStage | DopplStage;
type GrowthStage   = ProblemRecoveryStage['stage'] | DopplStage['stage'];
type NextOf<S extends Stage> = Extract<StageContract, { stage: S }>['next'];

The invariants that fall out of this: no doppl without a recovered problem; no problem without a case study; a problem may produce many doppls, each its own node.

6.3 Frontmatter is identity + routing

Frontmatter is a discriminated union on stage. The seed has no scores, no lineage, no dedup signal — it is a start, not a claim. Growth-stage nodes carry lineage, the judge score, the materialised human projection, and doppelgangers.

type Uuid = string;                         // UUIDv4; the durable link key
type NonEmptyArray<T> = [T, ...T[]];

type BaseFrontmatter<S extends Stage> = { id: Uuid; stage: S; next: NextOf<S> };

type CaseStudyFrontmatter = BaseFrontmatter<'case_study'> & { name: string };

type BaseGrowthFrontmatter<S extends GrowthStage> = BaseFrontmatter<S> & {
  root: Uuid;
  prev: NonEmptyArray<Uuid>;
  kernel?: KernelName;
  temporal: boolean;
  scores: Scores;            // judge + materialised human projection
  doppelgangers: number;
};

type NodeFrontmatter =
  | CaseStudyFrontmatter
  | BaseGrowthFrontmatter<'problem_recovery'>
  | BaseGrowthFrontmatter<'doppl'>;

Links point at id, never the headline. Names and headlines change freely; the UUID is the stable anchor. This single rule is what makes the whole thing safe to render into a graph database or an Obsidian vault without breaking links when text is edited.

6.4 The four body sections

Section Accretes? Meaning
## Trace yes One ### <Stage> · synopsis per completed prior stage, copied verbatim — never reworded or merged. Only the synopsis travels; full thinking stays home.
## Discovery yes What discovery found (web + stock), citing the stock field it read from or wrote to. Found, not concluded. Not scored.
## Growth replaced per node The current stage at full fidelity — what it concluded. The only section the judge rates. Holds ### Evaluation.
## Path Names the next stage, or null at a doppl. Intentionally duplicates frontmatter next so a reader never has to inspect YAML.

The headline (# …) is the one-line Growth result; on fold it becomes the next node's Trace synopsis.


7. MarkScript: the language

MarkScript is the framework that makes the node files both prose and data. It is worth treating as a first-class language because it is the load-bearing idea: it is what lets a folder of notes act like a typed graph database.

7.1 The three-layer section

Every MarkScript section has exactly three layers:

  1. meaning — what the section is for (prose);
  2. markdown shape — what the rendered artifact looks like;
  3. TypeScript contract — what a parser or validator must recover.

The TypeScript does not replace the markdown. It states what must survive parsing. The two structural primitives are tiny:

type MarkdownSection<Heading extends string, Body>    = { heading: Heading; body: Body };
type MarkdownSubsection<Heading extends string, Body> = MarkdownSection<Heading, Body>;
type MarkdownFile<Frontmatter, Body>                  = { frontmatter: Frontmatter; body: Body };

7.2 The five rules that keep it honest

Rule What it means
Build down Put primitives first, then base forms, then concrete variants, then the final union at the bottom. The reader should feel the thing being assembled — never open on a negation like NonSeedBody.
Information vs definition Every sentence earns its place as either a definition (names a thing) or information (explains behaviour). History — retired terms, rejected approaches — is neither, and is banned.
Type discipline If a type doesn't constrain, connect, or name a real parsed shape, delete it. never is allowed only when it does real work, never as a gravestone.
Source shape matters Prose uses soft wrap — one paragraph is one source line. Hard-wrapped prose makes the artifact worse.
Ownership One concept, one owner. rating.md owns Rating; node.md references it instead of restating it. Duplication is debt to collapse.

7.3 The test

A MarkScript section works when three readers can use it without the rest of the conversation:

flowchart LR
    MD["MarkScript section"] --> H["👤 Human<br/>reads the prose,<br/>understands the artifact"]
    MD --> P["⚙️ Parser<br/>finds required headings<br/>and payloads"]
    MD --> V["✅ Validator<br/>rejects drift<br/>without interpreting vibes"]
Loading

If it only serves one reader, it is not MarkScript yet. This triple is the elevator pitch made testable.


8. The engine: the generate→select crucible

The engine is the distilled contract of the kernel source (src/generate.ts, fitness.ts, select.ts, lens.ts, trace.ts). One spine arrow = one pass of the crucible over a population. It does not pick the best candidate; it breeds a stronger child, and the bar is anti-fragility — a child that gets stronger under variation.

8.1 The pass, end to end

sequenceDiagram
    participant Seed as Seed / parents
    participant Gen as generate
    participant Fit as fitness
    participant Sel as select
    participant Lens as lens
    participant Tr as trace / compile

    Seed->>Gen: SeedFixture (operators + source packets)
    Note over Gen: apply reproduction operator,<br/>attach lineage + delta,<br/>reject no-delta rehash
    Gen->>Fit: CandidatePool
    Note over Fit: novelty (0–1) + grounding (0–1)<br/>kept SEPARATE
    Fit->>Sel: ScoredCandidatePool
    Note over Sel: Pareto front → floor gate →<br/>directional score → keep top 3<br/>(+ regret sibling on the other dial)
    Sel->>Lens: SelectionComparison
    Note over Lens: feasibility, AFTER selection,<br/>never touches fitness
    Lens->>Tr: LensResult[]
    Note over Tr: RunTrace = the specimen —<br/>compiler projects survivor → node
Loading

8.2 Generate — operators, lineage, no-delta rejection

A candidate is bred by a named reproduction operator applied to a source packet. Each child carries lineage (parent, generation, operatorId) and an explicit delta: what changed besides wording. Packets with no delta are rejected before scoring — rehash never reaches fitness. (This is invariant #11: every child must state its delta.)

The operators come from mutagen skills — and this is the seam where your AI harness plugs in (Section 13). The kernel records which operator produced each child but never requires a specific external loader.

type CandidateDelta = { summary: string; changes: string[] };

type Candidate = {
  id: string;
  parentId: string;
  parent: { kind: 'seed' | 'candidate'; id: string };
  generation: number;
  operatorId: string;        // e.g. 'polymath', 'first-principles', 'breakout'
  temporal: boolean;
  title: string;
  thesis: string;
  substrate: string;
  mechanism: string;
  delta: CandidateDelta;     // rejected before scoring if empty
  claims: string[];
  evidence: string[];
};

8.3 Fitness — two axes, deterministic on purpose

Both axes are weighted sums of 0–1 components. They are deterministic text/source signals on purpose: novelty must point at absence-from-record (not "the model says it's novel"), and grounding must point outside the prose.

Novelty = 0.50·sourceAbsence + 0.30·substrateDistance + 0.20·hiddenDependents Grounding = 0.40·signalStrength + 0.25·mechanismClarity + 0.25·falsifiability − 0.10·riskPenalty

type FitnessScore = {
  novelty: number;     // 0–1, rounded to 3 dp, clamped
  grounding: number;   // 0–1
  decay: { factor: number; halfLifeDays: number; ageDays: number; temporalBasis: boolean };
  components: {
    sourceAbsence: number; substrateDistance: number; hiddenDependents: number;
    signalStrength: number; mechanismClarity: number; falsifiability: number; riskPenalty: number;
  };
  reasons: { novelty: string; grounding: string; decay: string };
};

8.4 Select — Pareto front, then directional rank, under floors

Per dial, a SelectionSchedule of { keep, priorityAxis, floorAxis, floor }:

dial priority floor axis floor
diverge novelty grounding 0.35
converge grounding novelty 0.25

The procedure preserves the two-axis tension before any scalar collapse:

  1. Pareto fronts — rank by non-domination over (novelty × grounding); front 1 is the frontier nothing beats on both.
  2. Floor gate — drop anything below the floor on the floor-axis.
  3. Directional score = priority·0.7 + secondary·0.2 + balanceBonus·0.1, where balanceBonus = 1 − |novelty − grounding|.
  4. Decay-adjust — currently a no-op (decayFactor = 1).
  5. Rank by front, then score, then priority axis; keep top 3.

The regret sibling. Every run computes both dials on the same scored pool and emits a cross-dial contrast per survivor — stable, replaced, or dropped. This is the proof the dial actually changes the run; a no-swap result is allowed data, not a failure.

8.5 The trace is the spine

One pass emits an ordered machine trace — generate → fitness → select → lens — each step naming inputs, decision, and goal-checks. Every human surface (node, board, viewer) is a projection of that trace. The boundary contracts are explicit and typed:

type BoundaryContract = {
  module: string;
  entersFrom: SpaceRef; input: ContractRef;
  output: ContractRef; exitsTo: SpaceRef;
};
// generate → fitness → select → lens → trace → ProofBoard

This is invariant #16 (trace first, views separate) and #19 (every boundary has a contract) — and it is what makes the system auditable rather than a black box.


9. Rating: measurements, judge, and the human ledger

There are exactly two numeric scales, and conflating them is the classic mistake.

type Measurement = number;                                   // 0…1; an instrument reading. No judgment.
type Rating = -5|-4|-3|-2|-1|0|1|2|3|4|5;                    // a judgment of worth.

Measurement is a reading (0.7 is just 0.7). Rating is worth — and crucially, negative does not mean "it doesn't work"; it means "even if it works, it is bad" (misleading, value-subtracting). 0 is neutral.

9.1 The bridge

A 0…1 measurement detects presence, so it maps only into the positive band:

rating = round(measurement * 5);   // 0 → 0, 0.5 → +3, 1 → +5

A measurement can never produce a negative rating. Negative ratings are judge-only — a human-or-model judgment that the idea subtracts value.

flowchart LR
    M["Measurements 0–1<br/>novelty · grounding · falsifiability"] -->|round of m×5| RB["Bridged ratings<br/>(positive band only)"]
    J["Judge (5 axes, −5…+5)<br/>+ full reasoning"] --> SJ["scores.judge<br/>= round(mean(axes))"]
    RB --> J
    JO["Judge-only axes<br/>Cost-efficiency · Relevance"] --> J
    H["Human: ONE slider −5…+5"] --> LED["human ratings ledger<br/>(node_id, rater_id)"]
    LED -->|project: mean, n| HP["scores.human · scores.n"]
    SJ --> NODE["node frontmatter<br/>scores: {judge, human, n}"]
    HP --> NODE
Loading

9.2 The five judge axes

Axis Reads Bridge
Novelty reaches something not already covered round(novelty × 5)
Grounding lands on something true / testable round(grounding × 5)
Falsifiability states what would make it wrong round(falsifiability × 5)
Cost-efficiency value vs. all-in ownership cost judge-only
Relevance matters for the current actor judge-only

The judge fills one #### <Axis> <±N> subsection per axis with reasoning; scores.judge is the boil-down. Humans never see this five-axis form — they get one slider. Asking a human to fill five axes will not happen in practice, so the system does not pretend it will.

9.3 The human ledger and the projection

Human ratings are the source of record and live in a separate ledger, upserted one current rating per (node_id, rater_id) (email for the demo). A later vote from the same email replaces the prior one — it does not add a second vote.

type HumanRating = { rater_id: string; score: Rating; rate_date: string /*ISO 8601*/ };
type HumanNodeRatings = { node_id: Uuid; ratings: HumanRating[] };

type HumanScoresProjection = {
  human: number | null;   // null when n === 0; else mean(scores), 1 decimal
  n: number;
};

The node stores only the materialised projection. A projection job (a local command, a scheduled job, a GitHub Action, or a service — intentionally open) reads HumanNodeRatings, computes the projection, and patches scores.human / scores.n back into frontmatter. A born node is judge-only: scores: { judge: 3, human: null, n: 0 }.

9.4 Temporal

temporal is the judge-set boolean seam. true = timing-bound and eligible for future decay; false = timeless. Decay is stubbed to 0, so the effective multiplier is 1 and ratings do not change with age — but a future time mechanism can bolt on without changing the node shape.


10. Discovery and stock: durable domain memory

10.1 Discovery is a tool, not a stage

Discovery is a round trip that problem_recovery and doppl call (never the case study — a seed doesn't search). On a call it reads stock for what's already known, reaches outward through a backend only when needed, keeps only what clears the bar, writes genuine new finds back to stock, and returns context to the calling stage. It is a modular interface — one verb (discover), pluggable backends (web search now, deep-research later).

Discovery is what was found; Growth is what was concluded.

flowchart LR
    Stage["problem_recovery / doppl"] -->|discover| D{Discovery}
    D -->|read first| Stock[(Stock field<br/>durable domain memory)]
    D -->|reach out when needed| Web[(Web / backend)]
    Web --> Adm{Admission gate<br/>novelty + grounding?}
    Adm -->|reject| Screened["Screened finds<br/>(audited, not stored as stock)"]
    Adm -->|admit| Enr{Enrichment gate}
    Enr -->|add| Stock
    Enr -->|merge| Stock
    Enr -->|drop rehash| X[discard]
    Stock -->|context| Stage
Loading

10.2 Two gates

  • Admission decides whether a find is worth remembering (requires both novelty and grounding signal). Low-value finds are screened — counted and auditable, but not stored as stock.
  • Enrichment decides what happens to an admitted find: add (new), merge (strengthen an existing discovery), or drop (rehash).
type StockSignal = { novelty: Measurement; grounding: Measurement };

type AdmittedDiscovery = {
  id: Uuid; field: FieldRef; claim: string; keywords: string[];
  sources: NonEmptyArray<SourceRef>; signal: StockSignal;
  created: string; updated: string;   // ISO 8601
};

Stock is not raw search output and not Growth. Discovery finds; stock remembers; stages conclude. The rendered stock field is a projection grouped by field, with discoveries and finds_screened counts in frontmatter — the human-readable "load-bearing facts" of a domain.


11. Prior art: where Doppl sits in the landscape

Doppl is not the first system to put LLM output under evolutionary selection — and knowing the neighbours sharpens what is genuinely distinctive here.

11.1 The closest cousins

System What it does Relationship to Doppl
AlphaEvolve (Google DeepMind) An evolutionary coding agent: an LLM ensemble proposes program mutations, automated evaluators verify them by execution, and an evolutionary loop keeps the best over many generations. It broke a 56-year matrix-multiplication record and improved Google's own infrastructure. Same skeleton — generate → evaluate → select → repeat. The hard difference: AlphaEvolve needs a programmatic ground-truth evaluator (you must be able to run and score the answer). Doppl's whole bet is the domains where no cheap ground truth exists ("is this a good idea?"), so it manufactures fitness from adversarial critics + two-axis measurement instead.
FunSearch / Evolution of Heuristics Earlier work pairing LLMs with evolutionary search to beat human baselines on math/heuristics. Proved the LLM-as-mutation-operator synergy Doppl inherits.
CodeEvolve (open source) Operationalises AlphaEvolve-style search openly using the island genetic algorithm: multiple populations evolve independently and periodically migrate their best individuals. This is your "islands of graphs" intuition, formalised. Doppl's islands are case-study-rooted; migration is the leaf→reseed and convergence edges.
MAP-Elites / quality-diversity Search that fills a grid of "niches" to keep a diverse set of high-performers rather than one optimum. Doppl's anti–mode-collapse pressure (novelty floor, Pareto fronts, sprouts) is the same instinct, expressed through the dial.
Obsidian / Zettelkasten + graph PKM Linked plain-markdown notes with a graph view; mature ecosystem of Neo4j bridges and MCP servers. This is Doppl's natural substrate and cockpit (Section 12) — but PKM has no engine. Doppl adds the selection loop on top of the vault.

11.2 What is genuinely distinctive

  1. Fitness without ground truth. AlphaEvolve climbs an executable score; Doppl climbs a manufactured one (two-axis measurement + adversarial critics + held-out human judgment as the un-fakeable anchor).
  2. The artifact is the medium. Most evolutionary systems store candidates as opaque rows in a programs database. Doppl's candidates are human-readable markdown under a typed contract — readable, linkable, gradable, and re-seedable by hand.
  3. One kernel, three dial settings. Discovery / problem-recovery / solution-finding are explicitly the same engine, not three pipelines — a strong unifying claim most stacks don't make.
  4. The trace is the truth, the node is a projection. Auditable by construction; the human surface can never silently diverge from what the machine actually did.

12. Implementation: the stack

The design goal is local-first, harness-agnostic, and substrate-portable. The same node files should work whether you open them in a text editor, an Obsidian vault, or a Neo4j browser. Below are four substrates, a decision matrix, and a recommended hybrid.

12.1 The four substrates

# Substrate What it is Best at Weak at
A Git + Markdown (plain) The node files in a repo; PRs are folds; history is lineage. Source of truth, diffability, zero lock-in, CI hooks (the projection job as a GitHub Action). No native graph view; querying "all doppls with judge ≥ +3 in island X" needs a script.
B Obsidian vault The same repo opened in Obsidian; [[wikilinks]] + frontmatter; built-in graph view; Dataview queries. The human cockpit — read, link, hand-prune sprouts, eyeball islands. Plugins for Neo4j, Mermaid, Dataview. Graph view is presentation, not a real query engine; large vaults get noisy.
C Local Neo4j (or FalkorDB) A derived property graph: nodes = Doppl nodes, edges = prev/root/convergence; Cypher for queries. Real graph queries & visualisation — convergence detection, lineage walks, "islands", Bloom visuals. A projection, not the truth; needs a sync job; another service to run.
D Hybrid (recommended) Git/markdown is canon; a parser projects into Neo4j; Obsidian is the editor; an MCP server exposes the graph to your agent. All of the above, with a single source of truth. Most moving parts (mitigated because the graph and vault are derived and disposable).

12.2 The recommended architecture

Git/markdown is the source of truth. Everything else is a disposable projection of it — which is exactly the system's own "trace is the specimen, views are projections" rule applied to the deployment.

flowchart TB
    subgraph CANON["Source of truth (canon)"]
        REPO[("Git repo<br/>nodes/*.md · stock/*.md<br/>specs/*.md (MarkScript contracts)")]
        LEDGER[("human-ratings ledger<br/>(JSON / table)")]
    end

    subgraph ENGINE["Kernel (TypeScript, local)"]
        KER["buildRunTrace()<br/>generate → fitness → select → lens"]
        COMP["compiler<br/>RunTrace → node.md"]
        PROJ["projection job<br/>ledger → scores.human / n"]
    end

    subgraph HARNESS["AI harness (yours)"]
        AG["Claude Code / Cursor<br/>drives the operators"]
        SK["mutagen skills<br/>polymath · first-principles · breakout …"]
        RULES["rules / invariants<br/>(CLAUDE.md · AGENTS.md · INVARIANTS.md)"]
    end

    subgraph VIEWS["Projections (disposable)"]
        OBS["Obsidian vault<br/>(human cockpit)"]
        NEO[("Neo4j / FalkorDB<br/>graph queries + Bloom")]
        BOARD["proof board<br/>(one-glance run summary)"]
    end

    AG <--> SK
    AG <--> RULES
    AG --> KER
    KER --> COMP --> REPO
    LEDGER --> PROJ --> REPO
    REPO -->|open directly| OBS
    REPO -->|parse + load| NEO
    KER --> BOARD
    NEO <-->|MCP server, read-only| AG
    OBS -.->|edit / hand-prune| REPO
Loading

12.3 Why this shape

  • Markdown canon honours invariant #23 (no silent source-of-truth split): the files say what they own; the graph and vault declare themselves projections.
  • Neo4j is derived, so a corrupted or stale graph is never a data-loss event — you rebuild it from the repo. The Obsidian→Neo4j bridge pattern (typed links - linkType [[note]], frontmatter → node properties) is well-trodden, and modern setups expose the graph to an agent over MCP, read-only, so the agent can query lineage and convergence without write access to your truth.
  • The proof board is the cheap, one-glance run summary: seed → generated → rejected → Explore keeps → Proof keeps → swap → failed checks. It is the first thing you look at; nodes are the drill-down.

12.4 Cypher you'd actually run

The graph projection earns its keep the moment you ask graph-shaped questions:

// Convergence: distinct islands arriving at the same attractor (a DERIVED query, never stored)
MATCH (a:Doppl)-[:ROOT]->(ra), (b:Doppl)-[:ROOT]->(rb)
WHERE ra <> rb AND a.embeddingCluster = b.embeddingCluster
RETURN a, b, a.embeddingCluster AS attractor;

// Process-health: doppelgangers piling up on low-rated ideas (the generator is stuck)
MATCH (n:Node)
WHERE n.doppelgangers >= 5 AND n.judge <= 0
RETURN n.id, n.headline, n.doppelgangers, n.judge ORDER BY n.doppelgangers DESC;

// The best leaves to reseed
MATCH (d:Doppl) WHERE d.judge >= 3 AND coalesce(d.human, d.judge) >= 3
RETURN d.id, d.headline, d.judge, d.human ORDER BY d.judge DESC LIMIT 10;

13. The AI harness layer: skills, rules, and the agentic pipeline

This is the part that makes Doppl runnable today, on your own machine, with the agent you already use. The repo already proves the pattern: it ships .cursor/skills/, a CLAUDE.md, an AGENTS.md, and INVARIANTS.md.

13.1 Operators are skills

Each reproduction operator is a mutagen skill — a SKILL.md with a persona, a mechanism, and a lineage block. The engine records which operator bred a child but never hard-codes a loader, so the same operator definitions work across harnesses.

rule-of-cool / breakthrough   gen-0 seed: converge on the best move within the frame
├── breakout                  valence-flip up: drop the feasibility filter, hunt the frame-breaker
├── blindside                 valence-flip down: hunt the buried failure mode / opportunity cost
├── addition-by-subtraction   best-cut instead of best-add: highest-leverage removal
├── first-principles          basis-transform: strip to invariants, rebuild
├── constraint-injection      scarcity operator: add the productive constraint
└── polymath                  domain-transfer: import a proven mechanism from another field (the Medici Effect)

polymath is the operator that most directly serves Doppl's two prey (cross-domain transfer, zeitgeist synthesis): it abstracts a problem to its structural essence, scans distant domains for where that structure is already solved, and ranks transplants by structural fit × domain distance — far enough to surprise, close enough to work.

13.2 Rules are invariants

The harness reads the invariants as guardrails — the rules that must survive any implementation change (finite by construction; novelty cannot be pure model self-grading; grounding must point outside the prose; append, do not overwrite; every child states its delta; trace first, views separate; every boundary has a contract). These map cleanly onto a Cursor .mdc rule file or a Claude Code project rule.

13.3 The pipeline as an agentic flow

sequenceDiagram
    actor U as You
    participant H as Harness (Claude Code / Cursor)
    participant S as Skills (operators)
    participant K as Kernel (TS)
    participant R as Repo (canon)
    participant V as Views (Obsidian / Neo4j)

    U->>H: seed a case_study (a situation / postulation)
    H->>R: write case_study node (no scores)
    U->>H: "recover the problem"
    H->>S: invoke operators (first-principles, polymath, …)
    S-->>H: candidate problem-frames (each with a delta)
    H->>K: score + select (converge: grounding priority)
    K-->>H: survivors + RunTrace + regret siblings
    H->>R: compile problem_recovery node(s)
    U->>H: "grow the doppls"
    H->>S: invoke operators (diverge: novelty priority)
    S-->>H: candidate unlocks
    H->>K: score + select + lens
    K-->>R: compile doppl leaves
    R->>V: project to vault + graph
    U->>V: read board, hand-prune sprouts, rate (one slider)
    U-->>R: reseed best leaf as a new case_study (close the loop)
Loading

The human stays in three spots only: seeding, one-slider rating, and hand-pruning sprouts / choosing which leaf to reseed. Everything else is the harness driving the kernel through the operators.


14. Strengths — conceptual and real

Conceptual strengths

  • A real unifying claim. "One engine, three dial settings, fitness that flips with the dial" is genuinely elegant and rare: it collapses discovery, problem-finding, and solution-finding into one auditable loop.
  • It names the failure modes and designs for them. Confident slop and mode collapse are not afterthoughts; the two-axis fitness, the floors, and the regret sibling exist specifically to keep both visible.
  • The artifact is the medium. Because every step is human-readable markdown under a typed contract, the system is legible at every stage — you can read why a node won, not just that it won.
  • Provenance and anti-fragility are first-class. Every child states its delta; lineage is append-only; the trace is the specimen. This is closer to a lab notebook than a black box.

Practical strengths

  • Harness-agnostic and local-first. No bespoke runtime: your existing coding agent supplies the intelligence; the kernel supplies control, scoring, and caps.
  • Portable substrate. The same files are a Git repo, an Obsidian vault, and a Neo4j projection — pick the view per task.
  • Finite by construction. Hard caps on generations, children, population (and, later, budget) make runaway recursion a configuration choice, not a risk.
  • De-risked seed. The generation-0 organism already exists as a working skill (the Rule of Cool / breakthrough progenitor); the work is mutating it into a population, not inventing from zero.

15. Drawbacks, risks, and where it breaks

Stated plainly, because the design's own ethos is "every proof needs a tripwire."

Risk Why it bites Mitigation already in the design
Fitness without ground truth (the core bet) If the critics/measurements are weak, evolution optimises for fooling the judge, not for good ideas. Ground critics in retrieval + executable checks where possible; hold human judgment out of the breeding loop as the un-fakeable anchor; rotate critic operators so the target keeps moving. The objective can evolve; its anchor cannot.
Mode collapse / slop convergence Populations love to converge on one safe, mediocre genome. Novelty floor, Pareto fronts, sprouts, and cross-distance fusion are explicit anti-collapse forces.
Deterministic measurements are shallow Token-overlap "novelty" can be gamed by surface wording; it is admittedly a placeholder. The contract keeps score detail rich enough to swap in embedding-cosine / cluster-coverage / DPP novelty when a named consumer needs it — without rewriting the corpus.
The second axis may be wrong Novelty × grounding may be less true than truth × consensus-gap (a claim can be true, novel, grounded — yet already priced by every serious observer). Flagged as an open question; the corpus is built to test it later.
Cost / termination Recursive spawning can blow a budget combinatorially. Structural caps today; budgeted energy caps (tokens, tool calls, wall-clock, money) are the next layer — a lineage that overruns dies or pauses, never borrows invisible compute.
Projection drift The Neo4j graph or Obsidian view can fall out of sync with the markdown canon. They are derived and disposable; a full recompute rebuilds them from the repo. The ledger projection job can run as a CI action to keep scores fresh.
Human-rating thinness One slider per (node, rater) is easy to gather but low-resolution, and small n makes human noisy. Intentional: five axes from humans won't happen in practice. Treat human as a coarse anchor, not a precise score; lean on n before trusting it.
Two-week-realism (capstone framing) The moonshot (self-evolving verifier, in-house fine-tuning flywheel) may not converge in the timebox. The single-generation fusion cut is the guaranteed-to-run MVP; the flywheel is explicitly stretch.

16. Phased build plan

flowchart LR
    P0["Phase 0<br/>Canon + contracts<br/>(specs/*.md, node template,<br/>case_study seeds)"]
    P1["Phase 1<br/>Single-generation kernel<br/>generate→fitness→select→lens<br/>on fixtures, proof board"]
    P2["Phase 2<br/>Compiler → node.md<br/>+ Obsidian vault view"]
    P3["Phase 3<br/>Graph projection (Neo4j)<br/>+ convergence / island queries"]
    P4["Phase 4<br/>Harness loop<br/>(skills as operators, rules,<br/>human one-slider + reseed)"]
    P5["Phase 5 (stretch)<br/>budget caps · richer novelty ·<br/>self-evolving critics · fine-tune flywheel"]
    P0 --> P1 --> P2 --> P3 --> P4 --> P5
Loading
  • Phase 0–1 is the SPEC's "definition of done": the kernel runs end-to-end on at least one real seed, produces a lineage tree + scored survivors a human can judge, and the same-seed diverge-vs-converge contrast is demonstrable.
  • Recursion is earned (invariant #5): don't deepen past depth-1 until a shallow run produces judgeable output and shows what to breed next.
  • Phase 3 onward is where the "islands of graphs" become queryable and visual, and where reseeding leaves closes the forest loop.

17. Appendix: glossary and contract index

17.1 Glossary (load-bearing terms)

Term Meaning
Kernel The reusable loop generate → evaluate → select → generate again, parameterised by direction, reproduction unit, fitness, and schedule.
Dial / direction The diverge ↔ converge knob; an oscillating schedule alternates them. The schedule is the application.
Node One markdown file = one step of an idea's journey; a projection of the RunTrace.
Stage case_study → problem_recovery → doppl. Each arrow is one crucible pass.
doppl The amorphous leaf: the unlock / solution / idea.
Pepsi The metaphor for one-vs-many doppls (the perfect Pepsi vs. the perfect Pepsis). Not a schema term.
Measurement (0–1) An instrument reading. No judgment. Maps into ratings.
Rating (−5…+5) A judgment of worth. Negative = value-subtracting, not merely ineffective.
Novelty / Grounding The two warring fitness axes; kept separate until selection.
Decay / temporal The time axis. temporal boolean is live; decay is stubbed to 0.
Lens Observer-relative feasibility, scored after selection; never contaminates fitness.
Doppelgangers Stored count of near-duplicates deduped into a node — the one fact dedup destroys.
Convergence Distinct lineages hitting the same target. Derived, never stored.
Sprout A rare high-novelty side-idea kept on the node for later; pruned by hand.
Bedrock An anchor the generator cannot move: executable check, held-out case, dated prediction, human judgment, replayable run.
Stock Durable domain memory: admitted discoveries, not raw search output and not Growth.
Island / forest One case-study tree is an island; reseeded leaves + convergence edges make the forest.

17.2 Contract index (who owns what)

Contract Owns File
Node File shape, stages, frontmatter union, the four body sections, headline, portable synopsis, doppelgangers specs/node.md
Rating Rating, Measurement, the bridge, the five judge axes, EvaluationSection, temporal policy specs/rating.md
Human ratings ledger HumanRating, upsert semantics, HumanScoresProjection, materialisation specs/human-ratings-ledger.md
Stock Fields, admitted discoveries, screened finds, admission + enrichment gates, rendered field specs/stock.md
MarkScript The three-layer idiom, build-down, type discipline, ownership rules my-docs/garden/markscript.md
Engine / SPINE / INVARIANTS The crucible, the dial, the two non-negotiables, the kernel invariants my-docs/garden/engine.md, SPINE.md, INVARIANTS.md
Kernel runtime buildRunTrace(), boundary contracts, the typed pipeline src/contracts/index.ts, src/trace.ts

The single rule that ties the whole document together: the markdown is the authored artifact; the TypeScript names what must be recoverable from it; and every human view — vault, graph, board — is a disposable projection of one machine trace that is the real specimen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment