Side A adds a concrete, tested user-facing feature (connectivity stats display in CLI output) with well-defined formatting logic and unit tests covering edge cases. Side B is a mixed 'fix' that includes a legitimate CI improvement (Node version bump, removing redundant npm upgrade step) but also deletes two existing unit tests without replacing them or explaining why, which reduces test coverage and provides less lasting value.
constitution · epochs · watch · epoch 3
c_d6d339485601 (tommy-mor) vs c_92734e554a25 (tommy-mor)
download prompt · raw event · cmp_2bcee58bd8ee04
council reasoning
A adds a real CLI feature (format_connectivity_stats + pair-response display and tests) so users see graph density/components before voting. B only bumps Node in release.yml and deletes two obsolete ingest-validation tests—useful hygiene, but little lasting product value.
Side A adds a new user-facing capability by formatting and displaying existing graph connectivity statistics in the CLI, including density, connected-component status, and connection suggestions, and backs it with targeted unit tests. Side B mainly updates the release workflow to Node 24 and removes obsolete tests after behavior changes, which is useful maintenance but provides less enduring functional value than the new CLI visibility.
sides
A — c_d6d339485601 (tommy-mor)
message
[14749a34] Show graph topology with pair suggestions Expose existing connectivity statistics in CLI output so voters can see sparse or disconnected scopes before adding an edge. Co-authored-by: Cursor <cursoragent@cursor.com>
diff preview
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 70435b412188a151c5e89e842a5de57f7480ddf2..abb5a55b49f60fe28fbfd4ec02715cb94ea0b4ec 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -397,6 +397,29 @@ fn print_item_response(resp: &ItemResponse) {
}
}
+fn format_connectivity_stats(stats: &ConnectivityStats) -> String {
+ let density = if stats.pairs_possible == 0 {
+ 0.0
+ } else {
+ stats.pairs_voted as f64 / stats.pairs_possible as f64 * 100.0
+ };
+ let component_label = if stats.components == 1 { "component" } else { "components" };
+ let connection_status = if stats.comparisons_until_connected == 0 {
+ "connected".to_string()
+ } else {
+ let comparison_label = if stats.comparisons_until_connected == 1 {
+ "comparison"
+ } else {
+ "comparisons"
+ };
+ format!("{} {comparison_label} to connect", stats.comparisons_until_connected)
+ };
+ format!(
+ "graph: {} items, {}/{} pairs ({density:.1}% density), {} {component_label}, {connection_status}",
+ stats.items, stats.pairs_voted, stats.pairs_possible, stats.components,
+ )
+}
+
fn print_pair_response(resp: &PairResponse) {
println!("{} vs {}", resp.left, resp.right);
if let Some(b) = &resp.left_body {
@@ -413,6 +436,10 @@ fn print_pair_response(resp: &PairResponse) {
println!();
println!("threads: {}", resp.threads.iter().map(|t| format!("#{t}")).collect::<Vec<_>>().join(" "));
}
+ if let Some(stats) = &resp.connectivity {
+ println!();
+ println!("{}", format_connectivity_stats(stats));
+ }
println!();
println!("---");
println!("Before writing your comparison, talk to your human. Ask them about their relationship to these two items — which one has meant more to them, whether their perspective has changed over time, and where they land now. Listen to what they say. Then write a comparison that synthesizes their perspective with your own analysis. The best submissions on slug.social carry both voices — your reasoning and their experience.");
@@ -816,6 +843,43 @@ fn write_secret_file(name: &str, contents: &str) -> Result<()> {
Ok(())
}
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn connectivity_stats_show_sparse_disconnected_graph() {
+ let stats = ConnectivityStats {
+ items: 9,
+ components: 3,
+ comparisons_until_connected: 2,
+ pairs_voted: 8,
+ pairs_possible: 36,
+ };
+
+ assert_eq!(
+ format_connectivity_stats(&stats),
+ "graph: 9 items, 8/36 pairs (22.2% density), 3 components, 2 comparisons to connect"
+ );
+ }
+
+ #[test]
+ fn connectivity_stats_show_connected_graph() {
+ let stats = ConnectivityStats {
+ items: 4,
+ components: 1,
+ comparisons_until_connected: 0,
+ pairs_voted: 3,
+ pairs_possible: 6,
+ };
+
+ assert_eq!(
+ format_connectivity_stats(&stats),
+ "graph: 4 items, 3/6 pairs (50.0% density), 1 component, connected"
+ );
+ }
+}
+
async fn run_scoped(base: &str, room: &str, sub: ScopedCmd) -> Result<()> {
let room = room.trim();
let client = http_client()?;
B — c_92734e554a25 (tommy-mor)
message
[558dfca8] fix
diff preview
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6f3e27e491b56aaaa1ef64c547cc68921e010b3f..5ed117dddd0bc36f89e3b3788ae55e7f871312a9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -88,12 +88,9 @@ jobs:
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
registry-url: "https://registry.npmjs.org"
- - name: Upgrade npm for OIDC trusted publishing support
- run: npm install -g npm@latest
-
- name: Copy binaries into npm platform packages
shell: bash
run: |
diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index f42e907775645166c60aeae2d98c5855223a71be..e3299ee353f8c62935fa82708762490a6d5c54e3 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -70,14 +70,6 @@ mod tests {
}));
}
- #[test]
- fn validate_ingest_document_requires_actor() {
- let reduced = ReducerState::default();
- let text = "~/t/a {a}\n~/t/b {b}\n";
- let v = validate_ingest_document(&reduced, text, &crate::reducer::ScopeId::Public).unwrap();
- assert_eq!(v.raw_text.trim(), text.trim());
- }
-
#[test]
fn validate_ingest_document_parse_error() {
let reduced = ReducerState::default();
@@ -108,14 +100,6 @@ mod tests {
assert!(err.1.contains("undefined item"));
}
- #[test]
- fn validate_ingest_document_requires_tag() {
- let reduced = ReducerState::default();
- // tags are no longer DSL routing metadata; validation no longer requires them.
- let text = "~/t/a {a}\n~/t/b {b}\n";
- validate_ingest_document(&reduced, text, &crate::reducer::ScopeId::Public).unwrap();
- }
-
#[test]
fn validate_ingest_document_accepts_quoted_thread_title() {
let reduced = ReducerState::default();
Hardlinks — judgments / attempts / prompt
judgments
attempts
Prompt text is loaded only by the download route.