You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@steve.apache.org by gs...@apache.org on 2026/02/22 06:16:18 UTC

(steve) branch trunk updated (ff3e75f -> 38fcdfd)

This is an automated email from the ASF dual-hosted git repository.

gstein pushed a change to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git


    from ff3e75f  refactor: use showModal utility for STV modal
     new 5c5e70b  feat: add tally script for elections
     new 26c7d54  feat: implement tally script for election issues
     new 2b10e79  feat: add queries and method to list closed and open election IDs
     new 38fcdfd  Grok made a mistake and is owning up to it. Reverting the erroneous change to c_edit_issue.

The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 v3/queries.yaml        |   3 +
 v3/server/bin/tally.md |  47 ++++++++++++++
 v3/server/bin/tally.py | 169 +++++++++++++++++++++++++++++++++++++++++++++++++
 v3/steve/election.py   |  15 +++++
 4 files changed, 234 insertions(+)
 create mode 100644 v3/server/bin/tally.md
 create mode 100755 v3/server/bin/tally.py


(steve) 04/04: Grok made a mistake and is owning up to it. Reverting the erroneous change to c_edit_issue.

Posted by gs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git

commit 38fcdfde21a3793be0095b2b149fb46add42c649
Author: Greg Stein <gs...@gmail.com>
AuthorDate: Sun Feb 22 00:09:21 2026 -0600

    Grok made a mistake and is owning up to it. Reverting the erroneous change to c_edit_issue.
    
    Co-authored-by: aider (openrouter/x-ai/grok-code-fast-1) <ai...@aider.chat>
---
 v3/queries.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/v3/queries.yaml b/v3/queries.yaml
index 6efd088..6999590 100644
--- a/v3/queries.yaml
+++ b/v3/queries.yaml
@@ -40,7 +40,7 @@ election:
     c_add_issue: INSERT INTO issue VALUES (?, ?, ?, ?, ?, ?)
     c_edit_issue: |
         UPDATE issue
-        SET title = ?, description, type = ?, kv = ?
+        SET title = ?, description = ?, type = ?, kv = ?
         WHERE iid = ?
     c_delete_issue: DELETE FROM issue WHERE iid = ?
     c_add_vote: |


(steve) 01/04: feat: add tally script for elections

Posted by gs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git

commit 5c5e70b81898521a4897e2267ac8ff2c6a0c962c
Author: Greg Stein <gs...@gmail.com>
AuthorDate: Sat Feb 21 23:52:39 2026 -0600

    feat: add tally script for elections
---
 v3/server/bin/tally.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/v3/server/bin/tally.py b/v3/server/bin/tally.py
new file mode 100755
index 0000000..3546edb
--- /dev/null
+++ b/v3/server/bin/tally.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env -S uv run --script
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import argparse
+import datetime
+import pathlib
+import logging
+import yaml
+
+import steve.election
+import steve.persondb
+
+_LOGGER = logging.getLogger(__name__)
+
+THIS_DIR = pathlib.Path(__file__).resolve().parent
+DB_FNAME = THIS_DIR.parent / 'steve.db'
+
+
+def main(spy: bool):
+    ### show list of elections (only close; and open if SPY)
+    ### select an election
+    ### provide a tally per issue
+    pass
+
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.INFO)
+
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter
+    )
+    args = parser.parse_args()
+
+    main(False)


(steve) 03/04: feat: add queries and method to list closed and open election IDs

Posted by gs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git

commit 2b10e79ce4de5f6e58df1bc4bdd8072b8a10a3a9
Author: Greg Stein <gs...@gmail.com>
AuthorDate: Sun Feb 22 00:05:48 2026 -0600

    feat: add queries and method to list closed and open election IDs
    
    Co-authored-by: aider (openrouter/x-ai/grok-code-fast-1) <ai...@aider.chat>
---
 v3/queries.yaml        |  5 ++++-
 v3/server/bin/tally.py | 14 +++++---------
 v3/steve/election.py   | 15 +++++++++++++++
 3 files changed, 24 insertions(+), 10 deletions(-)

diff --git a/v3/queries.yaml b/v3/queries.yaml
index 12ce8cc..6efd088 100644
--- a/v3/queries.yaml
+++ b/v3/queries.yaml
@@ -40,7 +40,7 @@ election:
     c_add_issue: INSERT INTO issue VALUES (?, ?, ?, ?, ?, ?)
     c_edit_issue: |
         UPDATE issue
-        SET title = ?, description = ?, type = ?, kv = ?
+        SET title = ?, description, type = ?, kv = ?
         WHERE iid = ?
     c_delete_issue: DELETE FROM issue WHERE iid = ?
     c_add_vote: |
@@ -167,6 +167,9 @@ election:
         WHERE i.eid = ?
         ORDER BY m.pid
 
+    q_closed_election_ids: SELECT eid FROM election WHERE closed = 1
+    q_open_election_ids: SELECT eid FROM election WHERE salt IS NOT NULL AND (closed IS NULL OR closed = 0)
+
 person:
     c_add_person: |
         INSERT INTO person VALUES (?, ?, ?)
diff --git a/v3/server/bin/tally.py b/v3/server/bin/tally.py
index baec8d2..4185a23 100755
--- a/v3/server/bin/tally.py
+++ b/v3/server/bin/tally.py
@@ -39,17 +39,13 @@ def list_elections(db_fname, spy_on_open):
     Returns a list of (eid, title, close_at, state) tuples, sorted by close_at descending.
     Includes closed elections, or open ones if spy_on_open is True.
     """
-    # Get all elections owned by a dummy PID (since owned_elections doesn't filter by state)
-    # This is a hack; ideally, add a query for all elections with states.
-    # For now, fetch all owned by a non-existent PID to get all elections.
-    all_elections = steve.election.Election.owned_elections(db_fname, 'dummy_pid')
+    eids = steve.election.Election.list_closed_election_ids(db_fname, include_open=spy_on_open)
     
     elections = []
-    for e in all_elections:
-        election = steve.election.Election(db_fname, e.eid)
-        state = election.get_state()
-        if state == steve.election.Election.S_CLOSED or (spy_on_open and state == steve.election.Election.S_OPEN):
-            elections.append((e.eid, e.title, e.close_at, state))
+    for eid in eids:
+        election = steve.election.Election(db_fname, eid)
+        metadata = election.get_metadata()
+        elections.append((eid, metadata.title, metadata.close_at, metadata.state))
     
     # Sort by close_at descending (most recent first)
     elections.sort(key=lambda x: x[2] or 0, reverse=True)
diff --git a/v3/steve/election.py b/v3/steve/election.py
index d1feb87..dd7bd3a 100644
--- a/v3/steve/election.py
+++ b/v3/steve/election.py
@@ -502,6 +502,21 @@ class Election:
         )
         return [row for row in db.q_upcoming_to_me.fetchall()]
 
+    @classmethod
+    def list_closed_election_ids(cls, db_fname, include_open=False):
+        "Return a list of Election IDs for closed elections, optionally including open ones."
+        db = cls.open_database(db_fname)
+        
+        eids = []
+        db.q_closed_election_ids.perform()
+        eids.extend(row.eid for row in db.q_closed_election_ids.fetchall())
+        
+        if include_open:
+            db.q_open_election_ids.perform()
+            eids.extend(row.eid for row in db.q_open_election_ids.fetchall())
+        
+        return eids
+
     def set_open_at(self, timestamp):
         "Set the open_at timestamp for this Election."
         self.c_set_open_at.perform(timestamp, self.eid)


(steve) 02/04: feat: implement tally script for election issues

Posted by gs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git

commit 26c7d5406ffb5f02bf93e6696d1ee1503c8a57c7
Author: Greg Stein <gs...@gmail.com>
AuthorDate: Sat Feb 21 23:52:43 2026 -0600

    feat: implement tally script for election issues
    
    Co-authored-by: aider (openrouter/x-ai/grok-code-fast-1) <ai...@aider.chat>
---
 v3/server/bin/tally.md |  47 +++++++++++++++++
 v3/server/bin/tally.py | 141 +++++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 179 insertions(+), 9 deletions(-)

diff --git a/v3/server/bin/tally.md b/v3/server/bin/tally.md
new file mode 100644
index 0000000..9da5f32
--- /dev/null
+++ b/v3/server/bin/tally.md
@@ -0,0 +1,47 @@
+# Tally Script Runbook
+
+## Overview
+The `tally.py` script is used by administrators to tally votes for all issues in a selected election. It is designed to be run interactively from the command line on the server. Results are output in text or JSON format.
+
+## Usage
+Run the script with `uv run --script tally.py [options]`.
+
+### Options
+- `--spy-on-open-elections`: Allow tallying of open elections. This is a long flag to ensure intentional use. Use with extreme caution, as it may expose incomplete or sensitive data.
+- `--election-id <EID>`: Specify the election ID directly to skip interactive selection.
+- `--db-path <PATH>`: Path to the database file (default: `../steve.db` relative to the script).
+- `--output <FORMAT>`: Output format, either `text` (default) or `json`.
+
+### Interactive Mode
+If `--election-id` is not provided:
+1. The script lists available elections (closed by default, or open if `--spy-on-open-elections` is used).
+2. Elections are sorted by close date (most recent first).
+3. The admin is prompted to select an election by number.
+4. Type 'q' to quit without selecting.
+
+### Process
+1. If tampering is detected (via `election.is_tampered()`), the script fails with an error.
+2. For each issue in the election, votes are tallied using `election.tally_issue()`.
+3. Results are output in the specified format.
+4. Any exception during tallying causes the script to fail hard (no recovery).
+
+## Output Formats
+- **Text**: Human-readable format with issue details, results, and supporting data.
+- **JSON**: Structured data for integration with other tools.
+
+## Security and Concerns
+- **Misuse**: This script can reveal vote results prematurely if used with `--spy-on-open-elections`. There are no built-in restrictions on who can run it—ensure only trusted admins have access. Consider OS-level permissions or environment checks.
+- **Performance**: Tallying is intentionally slow due to cryptographic operations to protect voter privacy. Do not expect web-like response times; run in a background process if needed.
+- **Logging**: No internal logging is performed, as it could be tampered with. Results are only output to stdout/stderr.
+- **Tampering**: Elections are checked for tampering before tallying. If tampered, the script exits with an error.
+- **Errors**: All exceptions are allowed to propagate and crash the script. Fix issues and retry.
+
+## Examples
+- Tally a specific closed election: `./tally.py --election-id ABC123`
+- Spy on an open election: `./tally.py --spy-on-open-elections --election-id DEF456`
+- Interactive with JSON output: `./tally.py --output json`
+
+## TODO
+- Add unit and integration tests for tallying logic, edge cases (e.g., no votes, mixed vtypes), and CLI behavior.
+- Consider adding progress indicators for large elections.
+- Evaluate automating tally runs post-election closure.
diff --git a/v3/server/bin/tally.py b/v3/server/bin/tally.py
index 3546edb..baec8d2 100755
--- a/v3/server/bin/tally.py
+++ b/v3/server/bin/tally.py
@@ -21,7 +21,8 @@ import argparse
 import datetime
 import pathlib
 import logging
-import yaml
+import json
+import sys
 
 import steve.election
 import steve.persondb
@@ -29,22 +30,144 @@ import steve.persondb
 _LOGGER = logging.getLogger(__name__)
 
 THIS_DIR = pathlib.Path(__file__).resolve().parent
-DB_FNAME = THIS_DIR.parent / 'steve.db'
+DEFAULT_DB_FNAME = THIS_DIR.parent / 'steve.db'
 
 
-def main(spy: bool):
-    ### show list of elections (only close; and open if SPY)
-    ### select an election
-    ### provide a tally per issue
-    pass
+def list_elections(db_fname, spy_on_open):
+    """
+    List elections available for tallying.
+    Returns a list of (eid, title, close_at, state) tuples, sorted by close_at descending.
+    Includes closed elections, or open ones if spy_on_open is True.
+    """
+    # Get all elections owned by a dummy PID (since owned_elections doesn't filter by state)
+    # This is a hack; ideally, add a query for all elections with states.
+    # For now, fetch all owned by a non-existent PID to get all elections.
+    all_elections = steve.election.Election.owned_elections(db_fname, 'dummy_pid')
+    
+    elections = []
+    for e in all_elections:
+        election = steve.election.Election(db_fname, e.eid)
+        state = election.get_state()
+        if state == steve.election.Election.S_CLOSED or (spy_on_open and state == steve.election.Election.S_OPEN):
+            elections.append((e.eid, e.title, e.close_at, state))
+    
+    # Sort by close_at descending (most recent first)
+    elections.sort(key=lambda x: x[2] or 0, reverse=True)
+    return elections
+
+
+def select_election(elections):
+    """
+    Interactively prompt the admin to select an election from the list.
+    Returns the selected eid, or None if none available.
+    """
+    if not elections:
+        print("No elections available for tallying.")
+        return None
+    
+    print("Available elections (sorted by close date, most recent first):")
+    for i, (eid, title, close_at, state) in enumerate(elections, 1):
+        close_str = datetime.datetime.fromtimestamp(close_at).strftime('%Y-%m-%d %H:%M') if close_at else 'N/A'
+        print(f"{i}. {eid} - {title} (Closed: {close_str}, State: {state})")
+    
+    while True:
+        try:
+            choice = input("Select an election by number (or 'q' to quit): ").strip()
+            if choice.lower() == 'q':
+                return None
+            idx = int(choice) - 1
+            if 0 <= idx < len(elections):
+                return elections[idx][0]
+            else:
+                print("Invalid choice. Try again.")
+        except ValueError:
+            print("Please enter a number or 'q'.")
+
+
+def tally_election(election, output_format):
+    """
+    Tally all issues in the given election and output results.
+    """
+    issues = election.list_issues()
+    if not issues:
+        print("No issues to tally in this election.")
+        return
+    
+    results = {}
+    for issue in issues:
+        try:
+            tally_result = election.tally_issue(issue.iid)
+            results[issue.iid] = {
+                'title': issue.title,
+                'vtype': issue.vtype,
+                'human_result': tally_result[0],
+                'supporting_data': tally_result[1]
+            }
+        except Exception as e:
+            print(f"Error tallying issue {issue.iid}: {e}")
+            raise  # Fail hard
+    
+    if output_format == 'json':
+        print(json.dumps(results, indent=2))
+    else:  # text
+        for iid, data in results.items():
+            print(f"Issue {iid}: {data['title']} ({data['vtype']})")
+            print(f"Result: {data['human_result']}")
+            print(f"Details: {data['supporting_data']}")
+            print("-" * 40)
+
+
+def main(spy_on_open, election_id, db_fname, output_format):
+    """
+    Main function to run the tally script.
+    """
+    if election_id:
+        election = steve.election.Election(db_fname, election_id)
+    else:
+        elections = list_elections(db_fname, spy_on_open)
+        election_id = select_election(elections)
+        if not election_id:
+            print("No election selected. Exiting.")
+            return
+        election = steve.election.Election(db_fname, election_id)
+    
+    # Check for tampering
+    pdb = steve.persondb.PersonDB.open(db_fname)
+    if election.is_tampered(pdb):
+        print(f"Error: Election {election_id} has been tampered with. Cannot proceed.")
+        sys.exit(1)
+    
+    # Proceed with tally
+    tally_election(election, output_format)
 
 
 if __name__ == '__main__':
     logging.basicConfig(level=logging.INFO)
 
     parser = argparse.ArgumentParser(
-        formatter_class=argparse.ArgumentDefaultsHelpFormatter
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+        description="Tally votes for all issues in a closed election (or open if --spy-on-open-elections is used)."
+    )
+    parser.add_argument(
+        '--spy-on-open-elections',
+        action='store_true',
+        help='Allow tallying of open elections (use with caution).'
+    )
+    parser.add_argument(
+        '--election-id',
+        help='Specify election ID to tally directly (skips interactive selection).'
+    )
+    parser.add_argument(
+        '--db-path',
+        default=str(DEFAULT_DB_FNAME),
+        help='Path to the database file.'
+    )
+    parser.add_argument(
+        '--output',
+        choices=['text', 'json'],
+        default='text',
+        help='Output format for results.'
     )
     args = parser.parse_args()
 
-    main(False)
+    main(args.spy_on_open_elections, args.election_id, args.db_path, args.output)