Side A removes a large body of stale/duplicated legacy files (several of which contain leftover terminal-prompt text, proving they were accidental artifacts, not live code) and tidies a brainstorming doc — a genuine reduction of confusing dead weight. Side B only adds a standalone CSS file with no accompanying template wiring shown and an unclear commit message ('?'), so its lasting value is unverifiable and likely just scaffolding for unfinished work.
constitution · epochs · watch · epoch 3
c_794fa48498b4 (tommy-mor) vs c_b447fc224eaf (tommy-mor)
download prompt · raw event · cmp_3575b154eecd16
council reasoning
B adds a real, reusable product stylesheet (sorter.css) that defines the app’s dark theme, layout, vote fields, rank lists, and breadcrumbs—ongoing UI value. A is mostly delete-only cleanup of already-quarantined legacy dumps (including shell-prompt paste artifacts) plus a short ideas note swap, which improves repo hygiene but does not add lasting product behavior.
Side B adds a new reusable stylesheet (`server/static/sorter.css`) defining theme variables and styles for buttons, forms, ranking lists, breadcrumbs, and parser UI, which is a concrete project asset. Side A primarily deletes legacy source files and replaces one design-notes document with another; while cleanup can be useful, the patch shown mostly removes functionality and documentation rather than adding lasting behavior or fixes.
sides
A — 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 omittedB — c_b447fc224eaf (tommy-mor)
message
[d61c0469] ?
diff preview
diff --git a/server/static/sorter.css b/server/static/sorter.css
new file mode 100644
index 0000000000000000000000000000000000000000..f5fb317a2c2a86d3dd00a639fff017fa3b8cc354
--- /dev/null
+++ b/server/static/sorter.css
@@ -0,0 +1,168 @@
+:root {
+ --bg: #0f1115;
+ --fg: #e8eaed;
+ --muted: #9aa0a6;
+ --accent: #7cacf8;
+ --panel: #1a1d24;
+ --border: #2a2f3a;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: system-ui, -apple-system, sans-serif;
+ background: var(--bg);
+ color: var(--fg);
+ line-height: 1.5;
+}
+
+.muted {
+ color: var(--muted);
+}
+
+.small {
+ font-size: 0.875rem;
+}
+
+.view-meta {
+ position: fixed;
+ top: 0.5rem;
+ right: 0.5rem;
+ font-size: 0.75rem;
+}
+
+.demo-panel {
+ max-width: 40rem;
+ margin: 4rem auto 2rem;
+ padding: 2rem;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+
+.demo-panel h1 {
+ margin-top: 0;
+}
+
+.btn-primary {
+ background: var(--accent);
+ color: #0f1115;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-size: 1rem;
+ cursor: pointer;
+}
+
+.btn-primary:hover {
+ filter: brightness(1.1);
+}
+
+code {
+ font-size: 0.85em;
+ background: var(--bg);
+ padding: 0.1em 0.35em;
+ border-radius: 3px;
+}
+
+.vote-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+.vote-fields label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.vote-fields input {
+ padding: 0.4rem 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+}
+
+.rank-list {
+ margin: 0;
+ padding-left: 0;
+ list-style: none;
+}
+
+.rank-list li {
+ margin-bottom: 0.35rem;
+}
+
+.rank-num {
+ color: var(--muted);
+}
+
+#parser-input {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font-size: 1rem;
+ font-family: inherit;
+ resize: vertical;
+}
+
+#parser-form .btn-primary {
+ margin-top: 0.5rem;
+}
+
+.breadcrumbs {
+ font-size: 0.875rem;
+ margin-bottom: 1rem;
+ color: var(--muted);
+}
+
+.breadcrumbs a {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.breadcrumbs a:hover {
+ text-decoration: underline;
+}
+
+.breadcrumbs .separator {
+ color: var(--muted);
+}
+
+.rank-list a {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.rank-list a:hover {
+ text-decoration: underline;
+}
+
+.entity-card h2 {
+ margin-top: 0;
+}
+
+.scope-name {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.rank-heading {
+ margin: 0.75rem 0 0.25rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.rank-gap {
+ text-align: center;
+ margin: 0.25rem 0;
+}
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.