Skip to content

feat(#126/#127): mlisp-bugs filter hooks + subscriber ask command#136

Open
denzuko wants to merge 6 commits into
developfrom
feat/126-127-bugs-filter-ask-command
Open

feat(#126/#127): mlisp-bugs filter hooks + subscriber ask command#136
denzuko wants to merge 6 commits into
developfrom
feat/126-127-bugs-filter-ask-command

Conversation

@denzuko

@denzuko denzuko commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Two features split out from #100 use cases 2 and 1 respectively.

#126 — mlisp-bugs pre/post-filter hook

Adds the same :pre-filter/:post-filter hook pattern that mlisp lists already have, now available for bug packages.

New functions:

  • bugs-set-option pkg key value — sets config on a bugs package (:pre-filter, :post-filter, :ai-ask)
  • bugs-invoke-filter filter-programs headers body-lines — delegates to invoke-filter-chain (same exit-code contract: 0=pass, 1=reject, 3=discard)

Updated: bugs-process-submit — pre-filter runs before submission (non-zero aborts); post-filter runs after archival, before distribution (annotation pattern).

Also fixed: bugs-process-submit was returning bug-id as an exit code — mlisp-bugs exited 1 on every first bug submission because bug-id=1 looked like an error. Now returns 0 on success.

New admin command: bugs-set-option <pkg> <key> <value>

#127 — subscriber ask command

:ask command on -request lists: Subject: ask <question> or first body line ask <question>.

If :ai-ask is configured (mlisp-admin set-option <list> ai-ask <neural-cmd>), pipes the question through pipe-through-command and replies with the output. Falls back to a helpful plain-text reply listing available subscriber commands when :ai-ask is not set or produces no output (e.g. neural endpoint unreachable).

Specs: 13/13 BATS (BF-1..6, ASK-1..7)

491/491 locally (413 BATS + 78 FiveAM).

denzuko added 3 commits June 19, 2026 11:23
## #130 -- Streaming base64 encoder

base64-encode-file-streaming writes to a stream in 57-byte input /
76-char output chunks (RFC 2045 line length). Never holds more than
one chunk in memory -- safe for arbitrarily large files.

base64-encode-file (legacy) now delegates to the streaming version
via with-output-to-string. distrib-get also updated to use streaming.

## #131 -- yEnc multipart chunking

distrib-file no longer rejects files above max-file-size-kb. Instead:

- Files at/below :segment-size-kb (default 750KB): single message,
  base64 encoded (streaming). Unchanged from before for small files.
- Files above :segment-size-kb: yEnc multipart segments, one message
  per segment, subject '[list-id] fname (N/total)'.

New functions (all exported for test suite):
  base64-encode-file-streaming (path out-stream)
  yenc-encode-byte (byte)       -- (byte+42)%256, escapes 214/224/227/19
  yenc-encode-bytes (bytes)     -- vector->string, 128-char line wrap
  yenc-decode-bytes (str)       -- inverse (for roundtrip verification)
  compute-segments (path bytes) -- list of (offset size) pairs
  segment-subject (list fname n total) -- '[list] fname (N/total)'
  yenc-segment-header (...)     -- =ybegin/=ypart fields (1-indexed)
  yenc-segment-footer (...)     -- =yend size/pcrc32

yEnc encoding note: the bytes to escape (214/224/227/19) are the INPUT
bytes whose encoded form (input+42)%256 is a critical char (NUL/LF/CR/=).
NOT the raw bytes 0/10/13/61 themselves. The escaped output is '='
followed by (encoded+64)%256, giving =@/=J/=M/=} respectively.
The spec also mentions dot (.) line-stuffing but that is handled by
the MTA transport layer, not the encoder.

## Spec fixes

YENC-1: corrected from testing raw bytes 0/10/13/61 (wrong) to
  testing input bytes 214/224/227/19 (correct).
YENC-2: updated to use bytes 65/214/224/66, expects k/=@/=J/l.
CHUNK-5: yEnc is 1-indexed (begin=1 not begin=0 for first segment).
DYENC-4: 1600KB / 750KB = ceil(2.13) = 3 segments, not 2.

478/478 (400 BATS + 78 FiveAM, was 468/468 -- +10 BATS DSTRM/DYENC).
## #126 -- mlisp-bugs pre/post-filter hooks on bugs-submit

src/bugs.lisp:
  bugs-set-option (pkg key value): set arbitrary plist keys on a bugs
    package entry. Supported: :pre-filter :post-filter :ai-ask.
  bugs-invoke-filter (filter-programs headers body-lines): delegates to
    invoke-filter-chain from src/filters.lisp. Same exit-code contract
    as mlisp list pre/post-filters (0=pass 1=reject 3=discard).
  bugs-process-submit: pre-filter runs before submission; non-zero exit
    aborts with exit 1. post-filter runs after archival, before
    distribution (annotation-only pattern: the post-filter modifies the
    distributed copy, not the archive).

  Also fixed: bugs-process-submit was returning bug-id (e.g. 1 for the
  first bug) which bugs-main.lisp used as the exit code, causing
  mlisp-bugs to exit 1 (error) on every first submission. Now returns 0
  on success. BF-1 test caught this pre-existing bug.

src/admin.lisp:
  bugs-set-option <pkg> <key> <value>: new admin subcommand. Registered
  in the dispatch table alongside bugs-report.

src/package.lisp: exports bugs-set-option, bugs-invoke-filter.

## #127 -- subscriber ask command

src/commands.lisp: :ask dispatch added. Matches 'ask' or 'ask <question>'
  in Subject or first body line.

src/requests.lisp:
  handle-ask-command: if :ai-ask is configured on the list (via
    mlisp-admin set-option <list> ai-ask <cmd>), pipes the question
    through pipe-through-command and replies with the output.
    Falls back to handle-ask-fallback (list info + command list) when
    :ai-ask is not configured or the command produces no output.
  handle-ask-fallback: plain-text fallback reply listing available
    subscriber commands.

src/main.lisp: :ask case added to the request command dispatch.
src/package.lisp: exports handle-ask-command.

## Specs (13/13 BATS)

BF-1..6: bugs-submit without filter, pre-filter allow/reject/annotate,
  post-filter invocation, bugs-set-option config storage.
ASK-1..7: ask dispatch (no AI), with neural stub, off by default,
  reply addressed to From:, subject echoed, Subject: dispatch,
  body-first-line dispatch.

491/491 (413 BATS + 78 FiveAM, was 478/478 on develop -- +13 BF/ASK
specs +2 FLT specs from cherry-picked distrib yEnc work that was
missing from this branch).
bugs-process-submit now returns 1 (integer) on pre-filter rejection
instead of always returning the bug-id. bugs-main.lisp's submit branch
was discarding the return value and always returning 0. Fix: use the
return value as the exit code when it's numeric.
CLAUDE.md: project-wide internal knowledge base for Claude sessions.
Covers: core architectural philosophy (email as actor-based pub/sub),
GitFlow/BDD workflow rules, system architecture (subgroups, filter
pipeline, distrib, microservice pattern), key design constraints
(zero-dep mlisp.asd, neural.sh scope, GPG out of scope), future
roadmap (Q3 2026 anon networks, RT4-TWG, Watchers series).

docs/skills/email-pubsub-architecture.md: reusable skill document
covering the actor model over email, mapping to conventional pub/sub
primitives, historical lineage (BITNET/FidoNet/LISTSERV/debbugs),
why this beats Redis+Celery (supply chain, no broker, 40yr operational
history), mlisp-specific architecture, when to use/not use the pattern,
and future work directions.

Key insight documented: email is inherently async -- latency inside
the SMTP transaction is invisible to users. Do not import web/chat
latency expectations into this domain. Processing time is not a
bottleneck concern at any realistic mailing list traffic level (floor:
NYCbug ~50 msgs/week; ceiling: Linux kernel ~2000 msgs/day).

@denzuko denzuko left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some issues

Comment thread src/admin.lisp Outdated
exit 0=pass 1=reject 3=discard
Space-separated list for a filter chain.
post-filter <path> Filter run after archival, before distribution.
ai-ask <cmd> Command for subscriber ask queries (neural.sh etc.)."

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neural.sh is to be implemented as a pre/post filter add-onif a milter/filter is just a script or program then if neural.sh is used in chain or on is own, why does need its own key?

Comment thread CLAUDE.md Outdated
- P2P mesh networks (FidoNet-style store-and-forward)
- Anonymous remailers (Type I/II) with mlisp as list management layer

**Intended audiences:**

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is internal notes not for public release; the tool is to be generic and flexible in use case not specifically targeted as an internal tool.

denzuko added 2 commits June 19, 2026 17:04
PR #136 feedback:

1. src/requests.lisp / src/admin.lisp:
   :ai-ask key removed. The ask handler now reads :ask-filter from
   list options -- a neutral key naming any program (not AI-specific).
   Using :pre-filter was also wrong: pre-filter runs on all inbound
   messages at delivery time; ask needs to invoke the filter directly
   against the question string only. :ask-filter is distinct from
   :pre-filter and invoked only within handle-ask-command via
   pipe-through-command (same mechanism as bugs-report --summarize).
   The operator configures any program: neural.sh, a lookup script,
   a KB query, etc. No AI-specific framing.

2. CLAUDE.md:
   Removed Watchers series reference from roadmap (internal project
   name). Removed deployment context section (operator-specific
   infrastructure). Tool described as generic and flexible.

3. docs/skills/email-pubsub-architecture.md:
   Removed operator attribution from mlisp entry.
   Generalised RT4-TWG reference to a generic standardisation note.

4. test/bats/test_mlisp_126_127.bats:
   ASK-2 updated: set-option ask-filter (not pre-filter, not ai-ask).
   ASK-3 description updated to reference ask-filter.

491/491, CI pending.
Corrected architectural error from previous iteration.

The ask command is a -request command. The user talks to mlisp.
mlisp's job is to recognise the command and let the filter pipeline
handle it. The filter is a complete self-contained actor:

  #!/bin/sh
  BODY=$(cat)
  REPLY=$(neural.sh "$BODY")
  ( echo "To: ..."; echo ""; echo "$REPLY" ) | sendmail -t
  exit 3  # discard: mlisp should not send a second reply

mlisp's pre-filter already runs before command dispatch (main.lisp
step 3x). If the list owner configures a pre-filter on -request, it
handles the ask interaction entirely -- no additional key, no special
integration point, no pipe-through-command in mlisp code.

handle-ask-command is now minimal:
  - Dispatched by -request when Subject/body starts with 'ask'
  - Sends a plain-text fallback (list info + commands) when no
    pre-filter is configured
  - When a pre-filter IS configured, it runs before handle-ask-command
    is even reached (main.lisp 3x), handles the response itself, and
    exits 3 (discard) so mlisp stops

Removed: :ask-filter key (unnecessary), pipe-through-command call
in handle-ask-command (wrong layer), ask-filter documentation in
bugs-set-option (not needed there either).

ASK-2 test updated: pre-filter is a complete actor that sends its
own reply and exits 3. mlisp never touches the response content.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant