Side A adds a substantial, tested feature (content-addressed evidence ledger, resumable ranking, HTML evidence graph, integration/unit tests) that materially extends the project's transparency and durability guarantees. Side B is purely deletion of dead/legacy files and one idea-note replacement—housekeeping with no functional value and no tests.
constitution · epochs · watch · epoch 3
c_efa306aa8040 (tommy-mor) vs c_794fa48498b4 (tommy-mor)
download prompt · raw event · cmp_593d667540acc7
council reasoning
Side A adds a durable, tested evidence ledger (content-addressed Evidence events, idempotent append, ranking resume, byte-faithful blobs) plus public HTML indexes for epochs/commits/comparisons/attempts/judgments—core product transparency. Side B only deletes unused legacy sources and swaps an ideas note, removing dead weight without improving the live system.
Side A adds substantial, lasting functionality: a versioned append-only evidence ledger, idempotent persistence with resumable ranking, content-addressed evidence records, HTML evidence browsing endpoints, richer audit links, API enhancements, and extensive integration/unit tests. Side B is primarily repository cleanup, deleting legacy files and replacing one ideas document, with little evidence of new project functionality or bug fixes.
sides
A — c_efa306aa8040 (tommy-mor)
message
[d1ad77cf] Add transparent HTML evidence graph for epochs and rankings. Persist verbatim commits, comparisons, attempts, and judgments in the ledger, serve them as linkable HTML indexes, and keep ranking resumable across restarts. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/constitution.py b/constitution.py
index a58257e1881b21d1d6fa8e68a3faa222e4f661ef..f819007252f435680b8356fb4da83469b21e33af 100644
--- a/constitution.py
+++ b/constitution.py
@@ -156,6 +156,8 @@ DEFAULT_REPOSITORIES = [
"refs": ["refs/heads/**"],
},
]
+PUBLIC_BASE_URL = os.environ.get("PUBLIC_BASE_URL", "https://token.slug.social").rstrip("/")
+EVIDENCE_SCHEMA_VERSION = 2
DEFAULT_CONTRIBUTORS = {
"tommy-mor": ["thmorriss@gmail.com"],
"christopher-whitman": [
@@ -206,6 +208,9 @@ class Emission:
ranking: dict # author -> score str
models_used: list
discovery_snapshot_id: str = "" # empty only for pre-discovery ledger history
+ evidence_schema_version: int = 1
+ ranking_run_id: str = ""
+ ranking_event_id: str = ""
@event
@@ -237,7 +242,299 @@ class GitDiscovery:
commits: list
+@event
+class Evidence:
+ """Versioned, content-addressed constitutional evidence envelope."""
+ schema_version: int
+ event_id: str
+ epoch: int
+ kind: str
+ recorded_at_ms: int
+ previous_event_sha256: str
+ payload: dict
+
+
store = JsonlStore(JSONL_PATH)
+_LEDGER_LOCK = asyncio.Lock()
+
+
+# ===========================================================================
+# §1e. EVIDENCE — content-addressed, append-only, publicly linkable
+# ===========================================================================
+#
+# Every epoch, commit, comparison input, provider attempt, and judgment is
+# recorded as Evidence. Authoritative payloads keep exact bytes as base64 plus
+# SHA-256; decoded text is for display only.
+
+
+def _canonical_json(obj) -> bytes:
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
+
+
+def _sha256_hex(data: bytes) -> str:
+ return hashlib.sha256(data).hexdigest()
+
+
+def _bytes_blob(data: bytes | str) -> dict:
+ raw = data.encode("utf-8") if isinstance(data, str) else data
+ return {
+ "encoding": "base64",
+ "data": base64.b64encode(raw).decode("ascii"),
+ "byte_length": len(raw),
+ "sha256": _sha256_hex(raw),
+ "text": raw.decode("utf-8", "replace"),
+ }
+
+
+def _decode_blob(blob: dict | None) -> bytes:
+ if not blob:
+ return b""
+ return base64.b64decode(blob["data"].encode("ascii"))
+
+
+def _content_id(prefix: str, material) -> str:
+ digest = _sha256_hex(_canonical_json(material) if not isinstance(material, bytes) else material)
+ return f"{prefix}_{digest}"
+
+
+def _html_escape(text: str) -> str:
+ return (
+ text.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace('"', """)
+ )
+
+
+def _evidence_path(kind: str, entity_id: str) -> str:
+ routes = {
+ "epoch": f"/epochs/{entity_id}",
+ "commit": f"/commits/{entity_id}",
+ "comparison": f"/comparisons/{entity_id}",
+ "attempt": f"/attempts/{entity_id}",
+ "judgment": f"/judgments/{entity_id}",
+ "event": f"/events/{entity_id}",
+ }
+ return routes[kind]
+
+
+def _evidence_url(kind: str, entity_id: str) -> str:
+ return PUBLIC_BASE_URL + _evidence_path(kind, entity_id)
+
+
+def _previous_event_sha256(events: list) -> str:
+ for event_ in reversed(events):
+ if isinstance(event_, Evidence):
+ return event_.event_id.split("_", 1)[-1]
+ if isinstance(event_, (GitDiscovery, Emission)):
+ return _sha256_hex(_canonical_json(to_dict(event_)))
+ return "0" * 64
+
+
+def _public_repo_row(repo: dict) -> dict:
+ """Strip credential-bearing clone URLs from published config."""
+ url = repo["url"]
+ if "@" in url and "://" in url:
+ scheme, rest = url.split("://", 1)
+ url = f"{scheme}://{rest.split('@', 1)[-1]}"
+ return {"id": repo["id"], "url": url, "refs": repo["refs"]}
+
+
+def _ledger_lock_path() -> pathlib.Path:
+ env = os.environ.get("LEDGER_LOCK_PATH")
+ if env:
+ return pathlib.Path(env)
+ return pathlib.Path(str(store.path) + ".lock")
+
+
+async def append_evidence(epoch: int, kind: str, payload: dict) -> Evidence:
+ """Durably append one Evidence event under process + file locks."""
+ # Logical identity ignores chain links / wall clock so restarts stay idempotent.
+ event_id = _content_id("ev", {
+ "schema_version": EVIDENCE_SCHEMA_VERSION,
+ "epoch": epoch,
+ "kind": kind,
+ "payload": payload,
+ })
+ async with _LEDGER_LOCK:
+ lock_path = _ledger_lock_path()
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
+ with lock_path.open("a+b") as lock_file:
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
+ try:
+ events = store.read()
+ existing = next(
+ (
+ e for e in events
+ if isinstance(e, Evidence) and e.event_id == event_id
+ ),
+ None,
+ )
+ if existing:
+ return existing
+ recorded_at_ms = int(time.time() * 1000)
+ previous = _previous_event_sha256(events)
+ evidence = Evidence(
+ schema_version=EVIDENCE_SCHEMA_VERSION,
+ event_id=event_id,
+ epoch=epoch,
+ kind=kind,
+ recorded_at_ms=recorded_at_ms,
+ previous_event_sha256=previous,
+ payload=payload,
+ )
+
+ def append_once(current):
+ if any(
+ isinstance(e, Evidence) and e.event_id == event_id
+ for e in current
+ ):
+ return None
+ return evidence
+
+ appended = await store.atomic(append_once)
+ result = appended or next(
+ e for e in store.read()
+ if isinstance(e, Evidence) and e.event_id == event_id
+ )
+ try:
+ with open(store.path, "rb") as fh:
+ os.fsync(fh.fileno())
+ except OSError:
+ pass
+ finally:
+ fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
+ await broadcast_audit(
+ kind,
+ f"{kind}: {payload.get('summary') or event_id[:24]}",
+ phase=PROCESS_STATE.get("phase"),
+ progress=PROCESS_STATE.get("progress"),
+ evidence_event_id=result.event_id,
+ evidence_url=_evidence_url("event", result.event_id),
+ )
+ return result
+
+
+def evidence_by_kind(kind: str | None = None) -> list[Evidence]:
+ rows = [e for e in store.read() if isinstance(e, Evidence)]
+ if kind is None:
+ return rows
+ return [e for e in rows if e.kind == kind]
+
+
+def find_evidence(event_id: str) -> Evidence | None:
+ return next(
+ (e for e in store.read() if isinstance(e, Evidence) and e.event_id == event_id),
+ None,
+ )
+
+
+def find_evidence_payload(kind: str, key: str, value: str) -> Evidence | None:
+ for e in evidence_by_kind(kind):
+ if e.payload.get(key) == value:
+ return e
+ return None
+
+
+def commit_id_for_oid(oid: str) -> str:
+ return _content_id("c", {"oid": oid})
+
+
+def comparison_id_for(material: dict) -> str:
+ return _content_id("cmp", material)
+
+
+def attempt_id_for(comparison_id: str, model_id: str, attempt_number: int) -> str:
+ return _content_id("att", {
+ "comparison_id": comparison_id,
+ "model_id": model_id,
+ "attempt_number": attempt_number,
+ })
+
+
+def judgment_id_for(material: dict) -> str:
+ return _content_id("jud", material)
+
+
+def _blob_text(blob) -> str:
+ if blob is None:
+ return ""
+ if isinstance(blob, str):
+ return blob
+ if isinstance(blob, dict):
+ if isinstance(blob.get("text"), str):
+ return blob["text"]
+ try:
+ return _decode_blob(blob).decode("utf-8", "replace")
+ except Exception:
+ return ""
+ return str(blob)
+
+
+def _discovery_for_epoch(epoch: int) -> GitDiscovery | None:
+ return next(
+ (
+ e for e in store.read()
+ if isinstance(e, GitDiscovery) and e.epoch == epoch
+ ),
+ None,
+ )
+
+
+def _emission_for_epoch(epoch: int) -> Emission | None:
+ return next(
+ (
+ e for e in store.read()
+ if isinstance(e, Emission) and e.epoch == epoch
+ ),
+ None,
+ )
+
+
+def _epochs_in_ledger() -> list[int]:
+ epochs: set[int] = set()
+ for e in store.read():
+ if isinstance(e, (GitDiscovery, Emission, Evidence)):
+ epochs.add(e.epoch)
+ return sorted(epochs)
+
+
+def _legacy_commit_row(commit_id: str) -> tuple[GitDiscovery | None, dict | None]:
+ for discovery in store.read():
+ if not isinstance(discovery, GitDiscovery):
+ continue
+ for commit in discovery.commits:
+ if commit_id_for_oid(commit["oid"]) == commit_id:
+ return discovery, commit
+ return None, None
+
+
+def _legacy_observation(commit_id: str) -> tuple[GitDiscovery | None, dict | None]:
+ for discovery in store.read():
+ if not isinstance(discovery, GitDiscovery):
+ continue
+ for obs in discovery.observations:
+ if commit_id_for_oid(obs["oid"]) == commit_id:
+ return discovery, obs
+ return None, None
+
+
+def build_pairwise_prompt(side_a: dict, side_b: dict) -> str:
+ return f"""You are ranking contributions to an open source project.
+Compare these two sides (each may be one or more commits). Decide which side contributed more.
+Return ONLY a JSON object: {{"winner": "A" or "B", "ratio": "N:M", "explanation": "..."}}
+
+Side A — commit messages:
+{side_a['message']}
+
+Side A — unified diffs (full patches):
+{side_a['diff']}
+
+Side B — commit messages:
+{side_b['message']}
+
+Side B — unified diffs (full patches):
+{side_b['diff']}"""
# ===========================================================================
@@ -565,45 +862,126 @@ async def fetch_top_models(n=3):
def _retry_llm_pairwise(exc: BaseException) -> bool:
- if isinstance(exc, (json.JSONDecodeError, KeyError, IndexError, TypeError)):
+ if isinstance(exc, (json.JSONDecodeError, KeyError, IndexError, TypeError, ValueError)):
return True
if isinstance(exc, httpx.HTTPStatusError):
return exc.response.status_code in (408, 425, 429, 500, 502, 503, 504)
return isinstance(exc, httpx.RequestError)
-@retry(
- retry=retry_if_exception(_retry_llm_pairwise),
- stop=stop_after_attempt(6),
- wait=wait_exponential(multiplier=1, min=1, max=120),
- reraise=True,
-)
-async def llm_pairwise_compare(model_id, side_a, side_b):
- prompt = f"""You are ranking contributions to an open source project.
-Compare these two sides (each may be one or more commits). Decide which side contributed more.
-Return ONLY a JSON object: {{"winner": "A" or "B", "ratio": "N:M", "explanation": "..."}}
+def _retry_wait_seconds(attempt_number: int) -> float:
+ return min(120.0, float(2 ** (attempt_number - 1)))
-Side A — commit messages:
-{side_a['message']}
-Side A — unified diffs (full patches):
-{side_a['diff']}
-
-Side B — commit messages:
-{side_b['message']}
-
-Side B — unified diffs (full patches):
-{side_b['diff']}"""
-
- async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=30.0)) as client:
- resp = await client.post(
- f"{OPENROUTER_BASE_URL}/api/v1/chat/completions",
- headers={"Authorization": f"Bearer {OPENROUTER_API_KEY}"},
- json={"model": model_id, "messages": [{"role": "user", "content": prompt}]},
+async def llm_pairwise_compar
… preview truncated; 68,950 characters omittedB — c_794fa48498b4 (tommy-mor)
message
[d2c766e1] cleanup
diff preview
diff --git a/ideas/parser.tdsl b/ideas/parser.tdsl
deleted file mode 100644
index 03ba9563ea8c73b7411cd1aa42dda9c7c313cd39..0000000000000000000000000000000000000000
--- a/ideas/parser.tdsl
+++ /dev/null
@@ -1,32 +0,0 @@
-The parser is a series of edges/transitions, for example:
-r -> reddit.com/
-where -> means that all of
- r->reddit.com/
- re->reddit.com/
- red->reddit.com/
- redd->reddit.com/
- reddi->reddit.com/
- reddit->reddit.com/
- reddit.->reddit.com/
- reddit.c->reddit.com/
- reddit.co->reddit.com/
- reddit.com->reddt.com/
- are defined as autocomplete suggestions
-
-re -> reddit.com/
-reddit.com/ -> {show infographic explaining that u (sort user posts) and r (sort subreddit posts)} -> reddit.com/r/
-reddit.com/r/ -> reddit.com/r/{randomly chose sub from list}
-reddit.com/r/{sub} -> {show subrdedit view} -> reddit.com/r/{sub}/
-reddit.com/r/{sub}/ -> {show infographic or something} -> reddit.com/r/{sub}/comments/{randomly chose comment from sql}
-
-
-h->https:// (show supported domains)
-
-// the edges are composable such that this is possible, while reusing the above reddit code.
-https://r->https://reddit.com/
-https://w->https://www.
-https://www.r->https://www.reddit.com/
-
-
-// and i also want to have commands after you press space, like
-reddit.com/r/programming !vote(reddit.comment{t3_123, t4_4444})%
diff --git a/ideas/url.tdsl b/ideas/url.tdsl
new file mode 100644
index 0000000000000000000000000000000000000000..d2397548d6ab2b8ca6af7a69782b809271536e9f
--- /dev/null
+++ b/ideas/url.tdsl
@@ -0,0 +1,4 @@
+ets zoom in on the data model. i want to import reddit data using the reddit api. i also want to import, eventually, every object on the internet.
+i want to be able to paste in a reddit url like
+https://old.reddit.com/r/AmItheAsshole/comments/1trnvdl/aita_for_cancelling_the_camping_trip_last_minute/
+and see the breadcrumbs (every segment of the url seperated by /) and click on each segment. as i click down, i want the children of that url to be visible and sorted. like the comments view of that one thread is visible with the full url, then i navigate to just r/amitheasshole, and i see every child of that node, whcih is all posts from that sub. then i nav to /r/ and i see all subs, sorted against eachother. etc. and to have this apply to every url on the intrenet eventually, but first work really well for reddit, with official reddit api support for importing data cleanly.
\ No newline at end of file
diff --git a/legacy/bundle.js b/legacy/bundle.js
deleted file mode 100644
index 8d316f9a57cc7c379fe4bd8e42e092c7eac5d265..0000000000000000000000000000000000000000
--- a/legacy/bundle.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Slug web UI: only plumbing — fetch/eval/SSE. No product UI logic here.
- */
-(function () {
- function evalJs(js) {
- if (js && String(js).trim()) {
- eval(js);
- }
- }
-
- // Theme cookie sync (runs before paint; full reload if localStorage disagrees with cookie)
-
- function initSlugUi() {
- // POST forms → eval response (except theme + full-navigation forms)
- document.addEventListener('submit', async function (e) {
- var f = e.target;
- if (!f || f.tagName !== 'FORM') return;
- if ((f.method || 'get').toLowerCase() !== 'post') return;
- if (f.id === 'slug-theme-form') return;
- if (f.getAttribute('data-navigate') === 'full') return;
- e.preventDefault();
- var resp = await fetch(f.action, {
- method: 'POST',
- body: new URLSearchParams(new FormData(f)),
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- credentials: 'same-origin',
- });
- evalJs(await resp.text());
- });
-
- // SSE: server-pushed JS
- function connectSSE() {
- var ssePath = window.location.pathname + window.location.search;
- var es = new EventSource('/sse?path=' + encodeURIComponent(ssePath));
- es.onmessage = function (e) {
- evalJs(e.data);
- };
- es.onerror = function () {
- es.close();
- setTimeout(connectSSE, 3000);
- };
- }
- connectSSE();
- }
-
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initSlugUi);
- } else {
- initSlugUi();
- }
-})();
-
diff --git a/legacy/clj-test.sh b/legacy/clj-test.sh
deleted file mode 100755
index b62a49b98ed60a65a46807b7ad80fa3142f14952..0000000000000000000000000000000000000000
--- a/legacy/clj-test.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-cd "$(dirname "$0")/.."
-mkdir -p target
-exec clojure -M:kaocha
diff --git a/legacy/event_log.rs b/legacy/event_log.rs
deleted file mode 100644
index eaae0d495e43a45d6590603892265a62cc92906e..0000000000000000000000000000000000000000
--- a/legacy/event_log.rs
+++ /dev/null
@@ -1,83 +0,0 @@
-use std::path::{Path, PathBuf};
-
-use tokio::{
- fs::{self, OpenOptions},
- io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
-};
-
-use crate::events::Event;
-
-#[derive(Debug, thiserror::Error)]
-pub enum EventLogError {
- #[error("io error: {0}")]
- Io(#[from] std::io::Error),
- #[error("json error: {0}")]
- Json(#[from] serde_json::Error),
-}
-
-#[derive(Debug, Clone)]
-pub struct EventLog {
- path: PathBuf,
-}
-
-impl EventLog {
- pub fn new(path: impl Into<PathBuf>) -> Self {
- Self { path: path.into() }
- }
-
- pub fn path(&self) -> &Path {
- &self.path
- }
-
- pub async fn ensure_parent_dir(&self) -> Result<(), EventLogError> {
- if let Some(parent) = self.path.parent() {
- fs::create_dir_all(parent).await?;
- }
- Ok(())
- }
-
- pub async fn append(&self, event: &Event) -> Result<(), EventLogError> {
- self.ensure_parent_dir().await?;
- let mut f: tokio::fs::File = OpenOptions::new()
- .create(true)
- .append(true)
- .open(&self.path)
- .await?;
-
- let mut line = serde_json::to_string(event)?;
- line.push('\n');
- f.write_all(line.as_bytes()).await?;
- f.flush().await?;
- Ok(())
- }
-
- /// Load events from JSONL. Corrupt lines are skipped and returned as `(line_no, line)`.
- pub async fn load_all(&self) -> Result<(Vec<Event>, Vec<(usize, String)>), EventLogError> {
- if !fs::try_exists(&self.path).await? {
- return Ok((vec![], vec![]));
- }
-
- let f = fs::File::open(&self.path).await?;
- let mut reader = BufReader::new(f).lines();
-
- let mut events = Vec::new();
- let mut bad_lines = Vec::new();
-
- let mut line_no: usize = 0;
- while let Some(line) = reader.next_line().await? {
- line_no += 1;
- let trimmed = line.trim();
- if trimmed.is_empty() {
- continue;
- }
- match serde_json::from_str::<Event>(trimmed) {
- Ok(ev) => events.push(ev),
- Err(_) => bad_lines.push((line_no, line)),
- }
- }
-
- Ok((events, bad_lines))
- }
-}
-
-
diff --git a/legacy/forms.rs b/legacy/forms.rs
deleted file mode 100644
index d509fd889fde3fed2662ce0a39006b7a51ae762a..0000000000000000000000000000000000000000
--- a/legacy/forms.rs
+++ /dev/null
@@ -1,145 +0,0 @@
-tommy@Tommys-Laptop:~/programming/slug-star/slug|main ⇒ cat server/src/form_template.rs
-//! Plan2-style JSON templates with `{"$form": "field_name"}` holes, filled from
-//! `application/x-www-form-urlencoded` (or any `String` → `String` map) **before**
-//! deserializing into a typed struct.
-//!
-//! # Wire format
-//!
-//! Templates are **compact JSON** (`serde_json::to_string`): one line, no pretty
-//! printing, strings escaped per JSON rules (`\"`, `\n`, etc.). Embed that string
-//! in HTML attributes or text nodes with normal HTML escaping (e.g. maud), not
-//! bespoke encodings.
-//!
-//! # Power vs flat hidden fields
-//!
-//! A form is always a string→string map. You can fake depth with dotted keys (`a.b.c`),
-//! but one structured blob (`__rpc__` = compact JSON) gives you nested objects,
-//! arrays, and optional fields without inventing a new naming scheme each time.
-//!
-//! # Security
-//!
-//! Substitution runs **before** `serde` into your command type. It does not fix
-//! authorization: if the client can replace the hidden `__rpc__` value, they can
-//! change the command shape unless you validate (signed blob, server-side session
-//! context, or treat the blob as hints only). Same threat model as any hidden field.
-
-use serde::Serialize;
-use serde_json::Value;
-use std::collections::HashMap;
-
-/// Serialize a value to compact JSON for a hidden `__rpc__` (or similar) field.
-pub fn template_json_compact<T: Serialize>(v: &T) -> serde_json::Result<String> {
- serde_json::to_string(v)
-}
-
-/// Recursively walk the JSON AST and replace `{"$form": "key"}` with the submitted
-/// string for `key` (empty if missing). Other keys are unchanged.
-pub fn substitute_form_vars(val: &mut Value, form_data: &HashMap<String, String>) {
- match val {
- Value::Object(map) => {
- if map.len() == 1 {
- if let Some(Value::String(field_name)) = map.get("$form") {
- let submitted = form_data
- .get(field_name.as_str())
- .map(|s| s.as_str())
- .unwrap_or("");
- *val = Value::String(submitted.to_string());
- return;
- }
- }
- for v in map.values_mut() {
- substitute_form_vars(v, form_data);
- }
- }
- Value::Array(arr) => {
- for v in arr.iter_mut() {
- substitute_form_vars(v, form_data);
- }
- }
- _ => {}
- }
-}
-
-/// Parse JSON, apply [`substitute_form_vars`], return the mutated value.
-pub fn fill_template_from_form(
- template_json: &str,
- form_data: &HashMap<String, String>,
-) -> Result<Value, serde_json::Error> {
- let mut v: Value = serde_json::from_str(template_json)?;
- substitute_form_vars(&mut v, form_data);
- Ok(v)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use serde::Deserialize;
-
- #[derive(Debug, Deserialize, PartialEq, Eq)]
- struct Demo {
- room: String,
- thread_tag: String,
- nested: Nested,
- }
-
- #[derive(Debug, Deserialize, PartialEq, Eq)]
- struct Nested {
- text: String,
- }
-
- #[test]
- fn holes_become_strings() {
- let json = r#"{
- "room": "public",
- "thread_tag": {"$form": "tag"},
- "nested": {"text": {"$form": "body"}}
- }"#;
- let mut form = HashMap::new();
- form.insert("tag".into(), "foo".into());
- form.insert("body".into(), "hello\nworld".into());
-
- let v = fill_template_from_form(json, &form).unwrap();
- let d: Demo = serde_json::from_value(v).unwrap();
- assert_eq!(
- d,
- Demo {
- room: "public".into(),
- thread_tag: "foo".into(),
- nested: Nested {
- text: "hello\nworld".into(),
- },
- }
- );
- }
-
- #[test]
- fn missing_form_key_is_empty_string() {
- let json = r#"{"x": {"$form": "nope"}}"#;
- let mut form = HashMap::new();
- form.insert("other".into(), "y".into());
- let v = fill_template_from_form(json, &form).unwrap();
- assert_eq!(v["x"], "");
- }
-
- #[test]
- fn array_of_holes() {
- let json = r#"{"items": [{"$form": "a"}, {"$form": "b"}]}"#;
- let mut form = HashMap::new();
- form.insert("a".into(), "1".into());
- form.insert("b".into(), "2".into());
-
… preview truncated; 125,831 characters omittedHardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.