diff --git a/README.md b/README.md
index 7a45249..47e5ae4 100644
--- a/README.md
+++ b/README.md
@@ -77,6 +77,8 @@ VirtualMe 把這個發現延伸成可上線的 pipeline:
## 8 週後你會拿到什麼
+> 🔎 **先看成品**:[`examples/sample-maker/`](examples/sample-maker/) 是用真實 export pipeline 從合成資料產出的 demo persona archive(虛構人物、雙語)。不用跑 8 週就能看到輸出長怎樣,從 [`START_HERE.md`](examples/sample-maker/START_HERE.md) 開始。
+
8 個 persona markdown 檔案(你的 archive;匯出時另含 `START_HERE.md`、`index.md`、`manifest.json` 作為入口、索引與機器可讀 metadata):
| 檔案 | 內容 |
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..6e7b82f
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,28 @@
+# Examples
+
+## `sample-maker/` — a generated demo persona archive
+
+This folder is a **real export from the VirtualMe pipeline**, so you can see what
+you get *before* committing to an 8-week interview.
+
+> ⚠️ **The persona is synthetic.** `sample-maker` is a fictional composite — not a
+> real person. The anchors are hand-written demo data, deliberately bilingual
+> (English + Traditional Chinese) to show the CJK-aware extraction path.
+
+What to look at:
+
+- **[`sample-maker/START_HERE.md`](sample-maker/START_HERE.md)** — the entry point a real user would read first.
+- **[`sample-maker/SOUL.md`](sample-maker/SOUL.md)** — core identity, with *triangulated* "Core Truths" (a principle that surfaced across ≥3 questions) separated from "Emerging Patterns", each with collapsible provenance.
+- **[`sample-maker/manifest.json`](sample-maker/manifest.json)** — the machine-readable index that ships beside the markdown.
+
+## Regenerate it
+
+The archive is produced by the real export pipeline from synthetic seed data:
+
+```bash
+python scripts/seed_demo.py
+```
+
+This seeds a throwaway database with the fictional persona, runs
+`virtualme.export.export_markdown`, and writes the archive to `examples/sample-maker/`.
+Edit the `ANCHORS` list in [`scripts/seed_demo.py`](../scripts/seed_demo.py) to change the demo.
diff --git a/examples/sample-maker/BOUNDARIES.md b/examples/sample-maker/BOUNDARIES.md
new file mode 100644
index 0000000..a016ced
--- /dev/null
+++ b/examples/sample-maker/BOUNDARIES.md
@@ -0,0 +1,61 @@
+---
+schema_version: "0.5"
+dimension: BOUNDARIES
+exported_at: 2026-05-31T06:28:05+00:00
+anchor_count: 3
+triangulated_count: 2
+emerging_count: 1
+anchor_content_pii_scrubbed: true
+---
+
+# BOUNDARIES
+
+Refusals, privacy rules, and persona update protocol.
+
+## Core Truths
+
+-
+
+ > Never lets an automated agent send anything outward without a human review step — draft → review → ship, every time.
+
+
+ Provenance
+
+ - Layer: principle
+ - Status: triangulated
+ - Questions: Q08, Q19, Q33
+ - Turns: 5
+
+
+
+-
+
+ > Keeps private/sensitive context local; refuses to put it in shared or third-party spaces.
+
+
+ Provenance
+
+ - Layer: principle
+ - Status: triangulated
+ - Questions: Q12, Q26, Q34
+ - Turns: 6
+
+
+
+
+## Emerging Patterns
+
+-
+
+ > 不在週末處理非緊急的工作訊息 — 保護休息與家庭時間。
+
+
+ Provenance
+
+ - Layer: pattern
+ - Status: emerging
+ - Questions: Q21
+ - Turns: 7
+
+
+
diff --git a/examples/sample-maker/HISTORY.md b/examples/sample-maker/HISTORY.md
new file mode 100644
index 0000000..8614d4b
--- /dev/null
+++ b/examples/sample-maker/HISTORY.md
@@ -0,0 +1,48 @@
+---
+schema_version: "0.5"
+dimension: HISTORY
+exported_at: 2026-05-31T06:28:05+00:00
+anchor_count: 2
+triangulated_count: 0
+emerging_count: 2
+anchor_content_pii_scrubbed: true
+---
+
+# HISTORY
+
+Life events and durable personal timeline context.
+
+## Core Truths
+
+_(no core truths yet)_
+
+## Emerging Patterns
+
+-
+
+ > Moved from a sales/operations role into product, then into building personal AI infrastructure as a long-running side project.
+
+
+ Provenance
+
+ - Layer: fact
+ - Status: emerging
+ - Questions: Q01
+ - Turns: 16
+
+
+
+-
+
+ > 曾把一個副專案從 demo 養成有真實使用者的小產品。
+
+
+ Provenance
+
+ - Layer: fact
+ - Status: emerging
+ - Questions: Q02
+ - Turns: 17
+
+
+
diff --git a/examples/sample-maker/JOURNAL.md b/examples/sample-maker/JOURNAL.md
new file mode 100644
index 0000000..44014dd
--- /dev/null
+++ b/examples/sample-maker/JOURNAL.md
@@ -0,0 +1,21 @@
+---
+schema_version: "0.5"
+dimension: JOURNAL
+exported_at: 2026-05-31T06:28:05+00:00
+anchor_count: 0
+triangulated_count: 0
+emerging_count: 0
+anchor_content_pii_scrubbed: true
+---
+
+# JOURNAL
+
+Reflections, interpretations, and periodic event notes.
+
+## Core Truths
+
+_(no core truths yet)_
+
+## Emerging Patterns
+
+_(no emerging patterns yet)_
diff --git a/examples/sample-maker/PEOPLE.md b/examples/sample-maker/PEOPLE.md
new file mode 100644
index 0000000..19604bf
--- /dev/null
+++ b/examples/sample-maker/PEOPLE.md
@@ -0,0 +1,48 @@
+---
+schema_version: "0.5"
+dimension: PEOPLE
+exported_at: 2026-05-31T06:28:05+00:00
+anchor_count: 2
+triangulated_count: 0
+emerging_count: 2
+anchor_content_pii_scrubbed: true
+---
+
+# PEOPLE
+
+Relationship context and recurring people schemas.
+
+## Core Truths
+
+_(no core truths yet)_
+
+## Emerging Patterns
+
+-
+
+ > Collaborates with a small set of named AI agents, each given one clear role rather than one agent doing everything.
+
+
+ Provenance
+
+ - Layer: fact
+ - Status: emerging
+ - Questions: Q20
+ - Turns: 14
+
+
+
+-
+
+ > Has a partner and family whose time is explicitly protected on the calendar.
+
+
+ Provenance
+
+ - Layer: fact
+ - Status: emerging
+ - Questions: Q25
+ - Turns: 15
+
+
+
diff --git a/examples/sample-maker/SKILL.md b/examples/sample-maker/SKILL.md
new file mode 100644
index 0000000..6be5f5c
--- /dev/null
+++ b/examples/sample-maker/SKILL.md
@@ -0,0 +1,62 @@
+---
+schema_version: "0.5"
+dimension: SKILL
+exported_at: 2026-05-31T06:28:05+00:00
+anchor_count: 3
+triangulated_count: 0
+emerging_count: 3
+anchor_content_pii_scrubbed: true
+---
+
+# SKILL
+
+Domain know-how, practices, and task preferences.
+
+## Core Truths
+
+_(no core truths yet)_
+
+## Emerging Patterns
+
+-
+
+ > Builds multi-agent systems with CLI + plain files instead of heavy frameworks; values things that still run when a vendor disappears.
+
+
+ Provenance
+
+ - Layer: pattern
+ - Status: emerging
+ - Questions: Q16, Q30
+ - Turns: 11
+
+
+
+-
+
+ > Runs a small home lab (single mini server + a VPS) with Docker; treats infrastructure as replaceable cattle, not pets.
+
+
+ Provenance
+
+ - Layer: fact
+ - Status: emerging
+ - Questions: Q17
+ - Turns: 12
+
+
+
+-
+
+ > 熟悉本地 LLM 部署、向量檢索與 RAG pipeline。
+
+
+ Provenance
+
+ - Layer: fact
+ - Status: emerging
+ - Questions: Q23
+ - Turns: 13
+
+
+
diff --git a/examples/sample-maker/SOUL.md b/examples/sample-maker/SOUL.md
new file mode 100644
index 0000000..d90c23c
--- /dev/null
+++ b/examples/sample-maker/SOUL.md
@@ -0,0 +1,75 @@
+---
+schema_version: "0.5"
+dimension: SOUL
+exported_at: 2026-05-31T06:28:05+00:00
+anchor_count: 4
+triangulated_count: 3
+emerging_count: 1
+anchor_content_pii_scrubbed: true
+---
+
+# SOUL
+
+Identity, values, and durable red lines.
+
+## Core Truths
+
+-
+
+ > Ships imperfect things on purpose — a working v0.5 today beats a perfect v1.0 that never ships. 寧可今天出一個會動的 v0.5,也不要永遠出不了的完美 v1.0。
+
+
+ Provenance
+
+ - Layer: principle
+ - Status: triangulated
+ - Questions: Q03, Q11, Q24
+ - Turns: 1
+
+
+
+-
+
+ > Treats reversible decisions as cheap and irreversible ones as expensive — moves fast on the former, slows right down on the latter.
+
+
+ Provenance
+
+ - Layer: principle
+ - Status: triangulated
+ - Questions: Q07, Q18, Q31
+ - Turns: 2
+
+
+
+-
+
+ > Optimises for future attention, not raw output — 最稀缺的資源是專注力,不是工時。
+
+
+ Provenance
+
+ - Layer: principle
+ - Status: triangulated
+ - Questions: Q05, Q22, Q29
+ - Turns: 3
+
+
+
+
+## Emerging Patterns
+
+-
+
+ > Reaches for the smallest experiment that could disprove an idea before committing to it.
+
+
+ Provenance
+
+ - Layer: pattern
+ - Status: emerging
+ - Questions: Q14
+ - Turns: 4
+
+
+
diff --git a/examples/sample-maker/START_HERE.md b/examples/sample-maker/START_HERE.md
new file mode 100644
index 0000000..0db0e38
--- /dev/null
+++ b/examples/sample-maker/START_HERE.md
@@ -0,0 +1,34 @@
+# Start Here: sample-maker
+
+This folder is your VirtualMe persona archive: human-editable Markdown files
+extracted from the interview process, with a machine-readable manifest beside them.
+
+- Exported at: 2026-05-31T06:28:05+00:00
+- Persona files: 8
+- Total anchors: 18
+- Core truths: 5
+
+## Recommended Reading Order
+
+1. [SOUL.md](SOUL.md) - core identity and values.
+2. [VOICE.md](VOICE.md) - how the agent should sound.
+3. [BOUNDARIES.md](BOUNDARIES.md) - what the agent should not do.
+4. [index.md](index.md) - complete file list and counts.
+
+## Archive Files
+
+- [SOUL.md](SOUL.md): Identity, values, and durable red lines.
+- [VOICE.md](VOICE.md): Voice patterns and reusable expression samples.
+- [SKILL.md](SKILL.md): Domain know-how, practices, and task preferences.
+- [PEOPLE.md](PEOPLE.md): Relationship context and recurring people schemas.
+- [HISTORY.md](HISTORY.md): Life events and durable personal timeline context.
+- [JOURNAL.md](JOURNAL.md): Reflections, interpretations, and periodic event notes.
+- [BOUNDARIES.md](BOUNDARIES.md): Refusals, privacy rules, and persona update protocol.
+- [STATE.md](STATE.md): Current-state snapshot that may become stale over time.
+
+## Machine-Readable Metadata
+
+- [manifest.json](manifest.json) contains schema version, counts, and payload file hashes.
+- Markdown frontmatter contains per-file dimension metadata.
+- Provenance details are folded under each item so the main text stays readable.
+- PII scrubbing applies to exported anchor content; archive IDs and folder names are unchanged.
diff --git a/examples/sample-maker/STATE.md b/examples/sample-maker/STATE.md
new file mode 100644
index 0000000..50cbb18
--- /dev/null
+++ b/examples/sample-maker/STATE.md
@@ -0,0 +1,34 @@
+---
+schema_version: "0.5"
+dimension: STATE
+exported_at: 2026-05-31T06:28:05+00:00
+anchor_count: 1
+triangulated_count: 0
+emerging_count: 1
+anchor_content_pii_scrubbed: true
+---
+
+# STATE
+
+Current-state snapshot that may become stale over time.
+
+## Core Truths
+
+_(no core truths yet)_
+
+## Emerging Patterns
+
+-
+
+ > Currently maintaining several small open-source repos that together form a personal knowledge stack.
+
+
+ Provenance
+
+ - Layer: fact
+ - Status: emerging
+ - Questions: Q32
+ - Turns: 18
+
+
+
diff --git a/examples/sample-maker/VOICE.md b/examples/sample-maker/VOICE.md
new file mode 100644
index 0000000..3824808
--- /dev/null
+++ b/examples/sample-maker/VOICE.md
@@ -0,0 +1,62 @@
+---
+schema_version: "0.5"
+dimension: VOICE
+exported_at: 2026-05-31T06:28:05+00:00
+anchor_count: 3
+triangulated_count: 0
+emerging_count: 3
+anchor_content_pii_scrubbed: true
+---
+
+# VOICE
+
+Voice patterns and reusable expression samples.
+
+## Core Truths
+
+_(no core truths yet)_
+
+## Emerging Patterns
+
+-
+
+ > Writes notes and commits in [Person A], code and APIs in English — 中英混用是常態,不刻意統一。
+
+
+ Provenance
+
+ - Layer: pattern
+ - Status: emerging
+ - Questions: Q09, Q27
+ - Turns: 8
+
+
+
+-
+
+ > Direct and a little blunt; skips hedging. Says 'this is wrong because X' rather than 'maybe consider X'.
+
+
+ Provenance
+
+ - Layer: pattern
+ - Status: emerging
+ - Questions: Q10, Q28
+ - Turns: 9
+
+
+
+-
+
+ > Prefers concrete examples over abstract advice — 「給我看 code」勝過「跟我講理論」。
+
+
+ Provenance
+
+ - Layer: fact
+ - Status: emerging
+ - Questions: Q15
+ - Turns: 10
+
+
+
diff --git a/examples/sample-maker/index.md b/examples/sample-maker/index.md
new file mode 100644
index 0000000..c9b5b83
--- /dev/null
+++ b/examples/sample-maker/index.md
@@ -0,0 +1,17 @@
+# VirtualMe Export: sample-maker
+
+- Generated at: 2026-05-31T06:28:05+00:00
+- Total anchors: 18
+- Triangulated principles: 5
+- Schema version: 0.5
+
+## Persona Files
+
+- [SOUL.md](SOUL.md): 4 anchors, 3 triangulated
+- [VOICE.md](VOICE.md): 3 anchors, 0 triangulated
+- [SKILL.md](SKILL.md): 3 anchors, 0 triangulated
+- [PEOPLE.md](PEOPLE.md): 2 anchors, 0 triangulated
+- [HISTORY.md](HISTORY.md): 2 anchors, 0 triangulated
+- [JOURNAL.md](JOURNAL.md): 0 anchors, 0 triangulated
+- [BOUNDARIES.md](BOUNDARIES.md): 3 anchors, 2 triangulated
+- [STATE.md](STATE.md): 1 anchors, 0 triangulated
diff --git a/examples/sample-maker/manifest.json b/examples/sample-maker/manifest.json
new file mode 100644
index 0000000..aa2b08e
--- /dev/null
+++ b/examples/sample-maker/manifest.json
@@ -0,0 +1,132 @@
+{
+ "schema_version": "0.5",
+ "export_id": "1d7957d9-e727-43bc-8838-d4f79cfac0b7",
+ "interviewee_id": "sample-maker",
+ "exported_at": "2026-05-31T06:28:05+00:00",
+ "persona_files": [
+ "SOUL.md",
+ "VOICE.md",
+ "SKILL.md",
+ "PEOPLE.md",
+ "HISTORY.md",
+ "JOURNAL.md",
+ "BOUNDARIES.md",
+ "STATE.md"
+ ],
+ "archive_files": [
+ "BOUNDARIES.md",
+ "HISTORY.md",
+ "JOURNAL.md",
+ "PEOPLE.md",
+ "SKILL.md",
+ "SOUL.md",
+ "START_HERE.md",
+ "STATE.md",
+ "VOICE.md",
+ "index.md",
+ "manifest.json"
+ ],
+ "human_entrypoint": "START_HERE.md",
+ "technical_index": "index.md",
+ "pii_scrub_scope": "anchor_content",
+ "dimensions": {
+ "SOUL": {
+ "file": "SOUL.md",
+ "description": "Identity, values, and durable red lines.",
+ "anchor_count": 4,
+ "triangulated_count": 3,
+ "emerging_count": 1
+ },
+ "VOICE": {
+ "file": "VOICE.md",
+ "description": "Voice patterns and reusable expression samples.",
+ "anchor_count": 3,
+ "triangulated_count": 0,
+ "emerging_count": 3
+ },
+ "SKILL": {
+ "file": "SKILL.md",
+ "description": "Domain know-how, practices, and task preferences.",
+ "anchor_count": 3,
+ "triangulated_count": 0,
+ "emerging_count": 3
+ },
+ "PEOPLE": {
+ "file": "PEOPLE.md",
+ "description": "Relationship context and recurring people schemas.",
+ "anchor_count": 2,
+ "triangulated_count": 0,
+ "emerging_count": 2
+ },
+ "HISTORY": {
+ "file": "HISTORY.md",
+ "description": "Life events and durable personal timeline context.",
+ "anchor_count": 2,
+ "triangulated_count": 0,
+ "emerging_count": 2
+ },
+ "JOURNAL": {
+ "file": "JOURNAL.md",
+ "description": "Reflections, interpretations, and periodic event notes.",
+ "anchor_count": 0,
+ "triangulated_count": 0,
+ "emerging_count": 0
+ },
+ "BOUNDARIES": {
+ "file": "BOUNDARIES.md",
+ "description": "Refusals, privacy rules, and persona update protocol.",
+ "anchor_count": 3,
+ "triangulated_count": 2,
+ "emerging_count": 1
+ },
+ "STATE": {
+ "file": "STATE.md",
+ "description": "Current-state snapshot that may become stale over time.",
+ "anchor_count": 1,
+ "triangulated_count": 0,
+ "emerging_count": 1
+ }
+ },
+ "payload_files": {
+ "BOUNDARIES.md": {
+ "sha256": "sha256:6197ecb310e6b29120a27ccaba7bd90b9494150f162809d9999f3d06765de5ad",
+ "bytes": 1033
+ },
+ "HISTORY.md": {
+ "sha256": "sha256:7758a0bfcb71240ad766555095335c33bb49e315a1f2acbe9a6c08bf692971b7",
+ "bytes": 777
+ },
+ "JOURNAL.md": {
+ "sha256": "sha256:9a243eb9bbeeca72230fa077993db8d013abae7fce87b43e95c41f65ebf9940b",
+ "bytes": 338
+ },
+ "PEOPLE.md": {
+ "sha256": "sha256:8f7e1e1400655cd7033a9114f14570382084cdb6104e45d1367d1e18d7d4ed46",
+ "bytes": 771
+ },
+ "SKILL.md": {
+ "sha256": "sha256:f8e4b16c6a305ca19e41b39e2e5e32906ad34f66598819391400411f99244f03",
+ "bytes": 1032
+ },
+ "SOUL.md": {
+ "sha256": "sha256:72c746dae91197e02c17d23e7f66e89c690bb8cd9cacdfa983eddf8e5306c076",
+ "bytes": 1374
+ },
+ "START_HERE.md": {
+ "sha256": "sha256:899d80b3a119d902795a57ee81cf909716c0075e8c36e8d5b8d5b35d28063aa8",
+ "bytes": 1541
+ },
+ "STATE.md": {
+ "sha256": "sha256:b15f4dbc1160880b867dba5b8fcd8701f8f08dca20a0959cde609b3234489819",
+ "bytes": 544
+ },
+ "VOICE.md": {
+ "sha256": "sha256:781d9c1255e226675b9a578770d30d1e63f7eedaaae7e80061e945f689b83ff2",
+ "bytes": 1044
+ },
+ "index.md": {
+ "sha256": "sha256:dc522cb4105b46945ecf91d21c0a0f3b8913a316a8fe8bbc48f085bb52c5a811",
+ "bytes": 584
+ }
+ }
+}
diff --git a/pyproject.toml b/pyproject.toml
index e3385ca..099fff0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,6 +57,7 @@ ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
"src/virtualme/snapshot/behavior_profile.py" = ["RUF001", "RUF002", "RUF003"]
+"scripts/seed_demo.py" = ["RUF001", "RUF002", "RUF003"]
[tool.pytest.ini_options]
testpaths = ["tests"]
diff --git a/scripts/seed_demo.py b/scripts/seed_demo.py
new file mode 100644
index 0000000..40c9713
--- /dev/null
+++ b/scripts/seed_demo.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+"""Seed a synthetic demo persona and export it to examples/.
+
+This populates a throwaway database with a SYNTHETIC, FICTIONAL persona
+(no real person) and runs the real export pipeline, so a fresh cloner can
+see what a VirtualMe persona archive looks like without doing 8 weeks of
+interviews.
+
+The persona is bilingual (English + Traditional Chinese) on purpose, to
+show the CJK-aware extraction path.
+
+Usage:
+ python scripts/seed_demo.py
+ python scripts/seed_demo.py --out examples --interviewee sample-maker
+"""
+
+from __future__ import annotations
+
+import argparse
+import asyncio
+import sys
+import tempfile
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
+
+from virtualme.export.markdown import export_markdown
+from virtualme.storage.db import DB, Dimension, Layer, init_db
+
+# --- SYNTHETIC DEMO DATA — fictional persona, not a real person ---
+# (dimension, layer, content, question_ids)
+# PRINCIPLE-layer anchors with >=3 distinct question_ids export as
+# "triangulated" core truths.
+ANCHORS: list[tuple[Dimension, Layer, str, list[str]]] = [
+ # SOUL — core identity / values (triangulated principles)
+ (Dimension.SOUL, Layer.PRINCIPLE,
+ "Ships imperfect things on purpose — a working v0.5 today beats a perfect "
+ "v1.0 that never ships. 寧可今天出一個會動的 v0.5,也不要永遠出不了的完美 v1.0。",
+ ["Q03", "Q11", "Q24"]),
+ (Dimension.SOUL, Layer.PRINCIPLE,
+ "Treats reversible decisions as cheap and irreversible ones as expensive — "
+ "moves fast on the former, slows right down on the latter.",
+ ["Q07", "Q18", "Q31"]),
+ (Dimension.SOUL, Layer.PRINCIPLE,
+ "Optimises for future attention, not raw output — 最稀缺的資源是專注力,不是工時。",
+ ["Q05", "Q22", "Q29"]),
+ (Dimension.SOUL, Layer.PATTERN,
+ "Reaches for the smallest experiment that could disprove an idea before "
+ "committing to it.",
+ ["Q14"]),
+ # BOUNDARIES — red lines (principles)
+ (Dimension.BOUNDARIES, Layer.PRINCIPLE,
+ "Never lets an automated agent send anything outward without a human review "
+ "step — draft → review → ship, every time.",
+ ["Q08", "Q19", "Q33"]),
+ (Dimension.BOUNDARIES, Layer.PRINCIPLE,
+ "Keeps private/sensitive context local; refuses to put it in shared or "
+ "third-party spaces.",
+ ["Q12", "Q26", "Q34"]),
+ (Dimension.BOUNDARIES, Layer.PATTERN,
+ "不在週末處理非緊急的工作訊息 — 保護休息與家庭時間。",
+ ["Q21"]),
+ # VOICE — how the agent should sound (patterns)
+ (Dimension.VOICE, Layer.PATTERN,
+ "Writes notes and commits in Traditional Chinese, code and APIs in English — "
+ "中英混用是常態,不刻意統一。",
+ ["Q09", "Q27"]),
+ (Dimension.VOICE, Layer.PATTERN,
+ "Direct and a little blunt; skips hedging. Says 'this is wrong because X' "
+ "rather than 'maybe consider X'.",
+ ["Q10", "Q28"]),
+ (Dimension.VOICE, Layer.FACT,
+ "Prefers concrete examples over abstract advice — 「給我看 code」勝過「跟我講理論」。",
+ ["Q15"]),
+ # SKILL — domain know-how (facts/patterns)
+ (Dimension.SKILL, Layer.PATTERN,
+ "Builds multi-agent systems with CLI + plain files instead of heavy "
+ "frameworks; values things that still run when a vendor disappears.",
+ ["Q16", "Q30"]),
+ (Dimension.SKILL, Layer.FACT,
+ "Runs a small home lab (single mini server + a VPS) with Docker; treats "
+ "infrastructure as replaceable cattle, not pets.",
+ ["Q17"]),
+ (Dimension.SKILL, Layer.FACT,
+ "熟悉本地 LLM 部署、向量檢索與 RAG pipeline。",
+ ["Q23"]),
+ # PEOPLE — relationships (facts)
+ (Dimension.PEOPLE, Layer.FACT,
+ "Collaborates with a small set of named AI agents, each given one clear role "
+ "rather than one agent doing everything.",
+ ["Q20"]),
+ (Dimension.PEOPLE, Layer.FACT,
+ "Has a partner and family whose time is explicitly protected on the calendar.",
+ ["Q25"]),
+ # HISTORY — life narrative (facts)
+ (Dimension.HISTORY, Layer.FACT,
+ "Moved from a sales/operations role into product, then into building personal "
+ "AI infrastructure as a long-running side project.",
+ ["Q01"]),
+ (Dimension.HISTORY, Layer.FACT,
+ "曾把一個副專案從 demo 養成有真實使用者的小產品。",
+ ["Q02"]),
+ # STATE — recent context (fact)
+ (Dimension.STATE, Layer.FACT,
+ "Currently maintaining several small open-source repos that together form a "
+ "personal knowledge stack.",
+ ["Q32"]),
+]
+
+
+async def seed_and_export(interviewee: str, out_dir: Path) -> list[Path]:
+ with tempfile.TemporaryDirectory() as tmp:
+ db_path = str(Path(tmp) / "demo.db")
+ await init_db(db_path)
+ db = DB(db_path)
+ await db.init()
+ for i, (dimension, layer, content, question_ids) in enumerate(ANCHORS):
+ await db.save_anchor(
+ interviewee_id=interviewee,
+ dimension=dimension,
+ layer=layer,
+ content=content,
+ source_turn_ids=[i + 1],
+ source_question_ids=question_ids,
+ model="synthetic-demo",
+ )
+ return await export_markdown(db, interviewee, out_dir)
+
+
+async def main() -> None:
+ parser = argparse.ArgumentParser(description="Seed a synthetic demo persona and export it.")
+ parser.add_argument("--interviewee", default="sample-maker")
+ parser.add_argument("--out", type=Path, default=Path("examples"))
+ args = parser.parse_args()
+
+ written = await seed_and_export(args.interviewee, args.out)
+ print(f"Exported {len(written)} files to {args.out / args.interviewee}/")
+ for path in written:
+ print(f" - {path}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/src/virtualme/snapshot/core.py b/src/virtualme/snapshot/core.py
index a2b34da..3679821 100644
--- a/src/virtualme/snapshot/core.py
+++ b/src/virtualme/snapshot/core.py
@@ -2,23 +2,27 @@
import json
import re
-import warnings
-from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Literal
-from pydantic import BaseModel, Field
-
from virtualme.interview.pii import scrub_pii
from virtualme.interview.triples import PersonaTriple
+from virtualme.snapshot.models import (
+ ConstructCard,
+ ConstructCardReview,
+ EvidenceItem,
+ MiniBlindTestItem,
+ ReviewEvidenceQuality,
+ ReviewVerdict,
+ SnapshotBundle,
+ SoulLiteHypothesis,
+ _Candidate,
+)
from virtualme.storage.db import DB, Anchor, Dimension, Layer
SNAPSHOT_SCHEMA_VERSION = "0.1"
-ReviewVerdict = Literal["like_me", "unlike_me", "unsure", "missing_context"]
-ReviewEvidenceQuality = Literal["none", "low", "medium", "medium_high", "high"]
-
CORE_DIMENSIONS = (
Dimension.SOUL,
Dimension.BOUNDARIES,
@@ -86,113 +90,6 @@
"它",
}
-warnings.filterwarnings(
- "ignore",
- message='Field name "register" in "ConstructCard" shadows an attribute in parent "BaseModel"',
- category=UserWarning,
-)
-
-
-class EvidenceItem(BaseModel):
- kind: str
- dimension: Dimension | None = None
- layer: Layer | None = None
- content: str
- source_anchor_ids: list[int] = Field(default_factory=list)
- source_turn_ids: list[int] = Field(default_factory=list)
- source_question_ids: list[str] = Field(default_factory=list)
- confidence: float | None = None
-
-
-class SoulLiteHypothesis(BaseModel):
- id: str
- dimension: Dimension
- hypothesis: str
- confidence: str
- evidence: list[EvidenceItem]
- missing_evidence: str
- suggested_follow_up: str
- needs_verification: bool
-
-
-class MiniBlindTestItem(BaseModel):
- id: str
- dimension: Dimension
- scenario: str
- what_to_compare: str
- evidence_hint: str
-
-
-class ConstructCard(BaseModel):
- id: str
- title: str
- decision_rule: str
- trigger_context: str
- protected_value: str
- traded_value: str | None = None
- default_action: str
- refused_action: str | None = None
- exception_rule: str | None = None
- register: str | None = None
- falsifier: str
- supporting_evidence: list[EvidenceItem]
- disconfirming_evidence: list[EvidenceItem]
- source_anchor_ids: list[int]
- source_turn_ids: list[int]
- source_question_ids: list[str]
- dimension_tags: list[Dimension]
- confidence_level: Literal["insufficient", "draft", "plausible", "validated"]
- confidence_reason: str
- confidence_checks: dict[str, bool]
- missing_evidence: list[str]
- blind_test_probe: str | None = None
- feedback_routes: list[str]
- extraction_method: Literal["rule_based", "llm_assisted", "human_curated"]
- policy_status: Literal["espoused_only", "behavior_supported", "contradicted", "validated"]
- stability_scope: str | None = None
- context_dependence: str | None = None
- exception_archetype: Literal[
- "relational_credit",
- "asymmetric_leverage",
- "operational_reciprocity",
- ] | None = None
-
-
-class ConstructCardReview(BaseModel):
- card_id: str
- verdict: ReviewVerdict
- reviewer: str | None = None
- reviewed_at: str | None = None
- notes: str | None = None
- concrete_case: str | None = None
- exception_note: str | None = None
- counterexample_note: str | None = None
- exact_wording_note: str | None = None
- pressure_note: str | None = None
- decision_tradeoff_note: str | None = None
- evidence_quality: ReviewEvidenceQuality = "none"
- status_after_review: str | None = None
- confidence_level: Literal["insufficient", "draft", "plausible", "validated"] | None = None
- policy_status: Literal["espoused_only", "behavior_supported", "contradicted", "validated"] | None = None
-
-
-class SnapshotBundle(BaseModel):
- schema_version: str = SNAPSHOT_SCHEMA_VERSION
- interviewee_id: str
- generated_at: str
- construct_cards: list[ConstructCard]
- hypotheses: list[SoulLiteHypothesis]
- mini_blind_test: list[MiniBlindTestItem]
- feedback_routes: list[str]
-
-
-@dataclass(frozen=True)
-class _Candidate:
- dimension: Dimension
- content: str
- evidence: EvidenceItem
- weight: int
-
async def build_snapshot_bundle(db: DB, interviewee_id: str) -> SnapshotBundle:
anchors = await db.load_anchors_summary(interviewee_id)
diff --git a/src/virtualme/snapshot/models.py b/src/virtualme/snapshot/models.py
new file mode 100644
index 0000000..a1b3622
--- /dev/null
+++ b/src/virtualme/snapshot/models.py
@@ -0,0 +1,119 @@
+from __future__ import annotations
+
+import warnings
+from dataclasses import dataclass
+from typing import Literal
+
+from pydantic import BaseModel, Field
+
+from virtualme.storage.db import Dimension, Layer
+
+ReviewVerdict = Literal["like_me", "unlike_me", "unsure", "missing_context"]
+ReviewEvidenceQuality = Literal["none", "low", "medium", "medium_high", "high"]
+
+warnings.filterwarnings(
+ "ignore",
+ message='Field name "register" in "ConstructCard" shadows an attribute in parent "BaseModel"',
+ category=UserWarning,
+)
+
+
+class EvidenceItem(BaseModel):
+ kind: str
+ dimension: Dimension | None = None
+ layer: Layer | None = None
+ content: str
+ source_anchor_ids: list[int] = Field(default_factory=list)
+ source_turn_ids: list[int] = Field(default_factory=list)
+ source_question_ids: list[str] = Field(default_factory=list)
+ confidence: float | None = None
+
+
+class SoulLiteHypothesis(BaseModel):
+ id: str
+ dimension: Dimension
+ hypothesis: str
+ confidence: str
+ evidence: list[EvidenceItem]
+ missing_evidence: str
+ suggested_follow_up: str
+ needs_verification: bool
+
+
+class MiniBlindTestItem(BaseModel):
+ id: str
+ dimension: Dimension
+ scenario: str
+ what_to_compare: str
+ evidence_hint: str
+
+
+class ConstructCard(BaseModel):
+ id: str
+ title: str
+ decision_rule: str
+ trigger_context: str
+ protected_value: str
+ traded_value: str | None = None
+ default_action: str
+ refused_action: str | None = None
+ exception_rule: str | None = None
+ register: str | None = None
+ falsifier: str
+ supporting_evidence: list[EvidenceItem]
+ disconfirming_evidence: list[EvidenceItem]
+ source_anchor_ids: list[int]
+ source_turn_ids: list[int]
+ source_question_ids: list[str]
+ dimension_tags: list[Dimension]
+ confidence_level: Literal["insufficient", "draft", "plausible", "validated"]
+ confidence_reason: str
+ confidence_checks: dict[str, bool]
+ missing_evidence: list[str]
+ blind_test_probe: str | None = None
+ feedback_routes: list[str]
+ extraction_method: Literal["rule_based", "llm_assisted", "human_curated"]
+ policy_status: Literal["espoused_only", "behavior_supported", "contradicted", "validated"]
+ stability_scope: str | None = None
+ context_dependence: str | None = None
+ exception_archetype: Literal[
+ "relational_credit",
+ "asymmetric_leverage",
+ "operational_reciprocity",
+ ] | None = None
+
+
+class ConstructCardReview(BaseModel):
+ card_id: str
+ verdict: ReviewVerdict
+ reviewer: str | None = None
+ reviewed_at: str | None = None
+ notes: str | None = None
+ concrete_case: str | None = None
+ exception_note: str | None = None
+ counterexample_note: str | None = None
+ exact_wording_note: str | None = None
+ pressure_note: str | None = None
+ decision_tradeoff_note: str | None = None
+ evidence_quality: ReviewEvidenceQuality = "none"
+ status_after_review: str | None = None
+ confidence_level: Literal["insufficient", "draft", "plausible", "validated"] | None = None
+ policy_status: Literal["espoused_only", "behavior_supported", "contradicted", "validated"] | None = None
+
+
+class SnapshotBundle(BaseModel):
+ schema_version: str = "0.1"
+ interviewee_id: str
+ generated_at: str
+ construct_cards: list[ConstructCard]
+ hypotheses: list[SoulLiteHypothesis]
+ mini_blind_test: list[MiniBlindTestItem]
+ feedback_routes: list[str]
+
+
+@dataclass(frozen=True)
+class _Candidate:
+ dimension: Dimension
+ content: str
+ evidence: EvidenceItem
+ weight: int