Skip to content

Add packaging and modernization foundation (Phase 0)#1

Merged
janlimpens merged 41 commits intomainfrom
claude/test-explore-4yhai
Mar 25, 2026
Merged

Add packaging and modernization foundation (Phase 0)#1
janlimpens merged 41 commits intomainfrom
claude/test-explore-4yhai

Conversation

@janlimpens
Copy link
Owner

  • Makefile.PL: require Perl 5.038, declare all CPAN prerequisites
  • cpanfile: project-local dependency manifest for Carton
  • .github/workflows/ci.yml: CI on Perl 5.38/5.40 (cpanm + make test)
  • CLAUDE.md: architecture reference for future development
  • .gitignore: add build artifacts (blib/, MYMETA.*, local/)
  • POPFile/Module.pm: add use parent, use feature signatures; remove
    Windows-specific pipe code and debug-only use DDP
  • POPFile/Loader.pm, Logger.pm: minor modernization
  • Services/IMAP.pm, IMAP/Client.pm: fix undef bug and regexp warnings
  • popfile.pl: minor cleanup
  • t/module.t: removed (replaced by proper test harness in Phase 1)

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

Jan Limpens and others added 30 commits March 24, 2026 00:12
- Makefile.PL: require Perl 5.038, declare all CPAN prerequisites
- cpanfile: project-local dependency manifest for Carton
- .github/workflows/ci.yml: CI on Perl 5.38/5.40 (cpanm + make test)
- CLAUDE.md: architecture reference for future development
- .gitignore: add build artifacts (blib/, MYMETA.*, local/)
- POPFile/Module.pm: add use parent, use feature signatures; remove
  Windows-specific pipe code and debug-only `use DDP`
- POPFile/Loader.pm, Logger.pm: minor modernization
- Services/IMAP.pm, IMAP/Client.pm: fix undef bug and regexp warnings
- popfile.pl: minor cleanup
- t/module.t: removed (replaced by proper test harness in Phase 1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- t/lib/TestHelper.pm: minimal wiring harness — real Configuration,
  stub Logger, synchronous-dispatch MQ stub, tempdir isolation
- t/00-load.t: smoke-loads all 15 core modules
- t/wordmangle.t: mangle() normalisation, regex metachars, length/hex
  filters, colon handling, stopword add/remove
- t/configuration.t: parameter CRUD, save/load, dirty flag, path sandbox
- t/mailparse.t: parse_file(), word extraction, header parsing
- t/bayes-classify.t: integration — session mgmt, bucket CRUD,
  train×5 + classify against ham/spam fixtures
- t/fixtures/ham.eml, spam.eml: deterministic fixture emails
- cpanfile.snapshot: Carton lockfile (deps in local/, gitignored)
- POPFile/Module.pm: drop debug-only `use DDP`

Run via: carton exec prove t/
All 51 assertions pass under perlbrew perl-5.42.1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop all MSWin32/Win32 conditionals — Linux/macOS only going forward.

- Platform/MSWin32.pm: deleted
- POPFile/Loader.pm: remove on_windows__ flag, CORE_platform_() method,
  Windows pipe-size hack in pipeready, Windows SIGCHLD/reaper workaround
- POPFile/MQ.pm: remove ActivePerl pipe-cache workaround in read_pipe_
- POPFile/Module.pm: remove Win32 select() bypass in slurp_; simplify
  can_read__ (non-socket Win32 path was always-true special case)
- Classifier/Bayes.pm: remove Kakasi mutex setup for Win32 threading;
  remove SQLite path UTF-8 conversion via File::Glob::Windows
- Proxy/POP3.pm: force_fork default is now 1 (was 0 on Windows);
  remove SSL+fork error message for Windows threads
- Proxy/NNTP.pm, SMTP.pm: force_fork default is now 1
- Proxy/Proxy.pm: remove pipe_cache__ field; $wait is always 0
- UI/HTML.pm: remove config path charset conversion for Windows/Nihongo

All tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace @isa assignments with `use parent '...'` in all 13 subclasses
  (Classifier, POPFile, Proxy, Services, UI namespaces)
- Convert 2-arg bareword open() to 3-arg lexical-filehandle form in:
  MailParse.pm, Bayes.pm (DBFILE, SCHEMA×2, MSG), Loader.pm (VER, MODULE),
  API.pm (IN/OUT), HTML.pm (MESSAGE)
  Note: echo_to_dot_() keeps 2-arg form (caller embeds mode in path arg)
- Replace indirect method call syntax `new Foo` with `Foo->new()` in:
  Bayes.pm, Module.pm (×3), Proxy.pm (×3), HTTP.pm, XMLRPC.pm (×2)

All tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POPFile::Mutex
- Standalone class with no inheritance
- field $name__, field $locked__; BUILD takes the mutex name
- method acquire($timeout=undef), method release
- Removed manual bless

Classifier::WordMangle
- class :isa(POPFile::Module) — inherits from bless-based parent
- field %stop__ replaces $self->{stop__}
- All methods converted: start, load/save_stopwords, mangle,
  add/remove_stopword, stopwords accessor
- BUILD sets module name; no manual new() needed
- 2-arg open calls converted to 3-arg lexical form
- Method signatures used throughout

POPFile::Module (cleanup, not full Object::Pad yet)
- Remove debug/np() development leftover from start()
- Remove unused debug() method that called Data::Printer np()

t/wordmangle.t
- 'stopwords' subtest: replaced direct $wm->{stop__} hash access
  with add_stopword/remove_stopword (Object::Pad fields are not
  accessible as hash keys)

Note: POPFile::Module (Phase 4c) deferred — converting the base
class requires all subclasses to be updated simultaneously.

All tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POPFile::Module: class :repr(HASH) with BUILD-initialized hash keys;
  all methods use Object::Pad method keyword with signatures
- POPFile::Logger: class :isa(POPFile::Module); BUILD sets name/slots;
  2-arg open DEBUG converted to 3-arg lexical
- POPFile::Configuration: class :isa(POPFile::Module); BUILD sets all
  slots; 2-arg/bareword opens (PID, CONFIG) converted to 3-arg lexical
- POPFile::MQ: class :isa(POPFile::Module); BUILD sets queue/waiters/
  children hashes and captures parent $$
- POPFile::History: class :isa(POPFile::Module); all 26 subs converted
  to method with signatures; 2-arg opens (FILE, CLASS) fixed; indirect
  flush STDOUT → STDOUT->flush()

All 39 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MailParse no longer holds a direct reference to Bayes. Instead:
- Replace {bayes__}, {color__}, {color_matrix__}, {color_idmap__},
  {color_userid__} with a single {color_resolver} sub ref
- get_color__ delegates to the closure; guards use defined(color_resolver)
- Bayes::get_html_colored_message sets a simple closure: $word → get_color
- Bayes::fast_get_html_colored_message sets a closure encapsulating the
  matrix/idmap reverse lookup (moved from MailParse into Bayes)
- After parse_file returns, color_resolver is set to undef

Circular dependency MailParse ↔ Bayes is broken; MailParse is now
testable without a Bayes instance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- class Classifier::Bayes :isa(POPFile::Module) with BUILD block
- All ~80 subs converted to methods with proper signatures
- substr_euc__ kept as plain sub (no $self)
- flush STDOUT → STDOUT->flush() (indirect method syntax banned in class blocks)
- db_update_cache__ params made optional ($updated_bucket, $deleted_bucket)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Services::Classifier wraps Classifier::Bayes with internal session
management — callers get a session-less API (no $session arg).
Proxy::Proxy, POP3, SMTP, NNTP converted to Object::Pad classes.
POPFile::Loader wires service to proxy/interface via set_service().
flush STDOUT → STDOUT->flush() in Loader.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(Phase 10/11)

Phase 10 — Mojolicious + Svelte UI:
- UI/Mojo.pm: POPFile::Module serving REST API on port 8080 and static
  files from public/; forks a Mojo::Server::Daemon child so it does not
  block the service() loop; handles its own DB session in the child
- REST endpoints at /api/v1/{buckets,history,magnets,magnet-types,config}
  covering full CRUD for buckets/magnets, paginated search history,
  per-slot reclassification, and config r/w
- SPA falls back to index.html for all non-API, non-asset paths (client-side routing)
- ui/: Svelte 5 + Vite SPA — App.svelte shell with hash routing plus
  four views: History, Corpus, Magnets, Security
- public/: pre-built bundle (index.html + hashed CSS/JS assets)
- Classifier/MailParse.pm: completed Object::Pad conversion
- cpanfile: add Mojolicious >= 9.0, JSON::MaybeXS

Phase 11 — Loader Object::Pad:
- POPFile/Loader.pm: class POPFile::Loader replacing bless-style new()
- All subs → Object::Pad methods with signatures and default params
- Bareword filehandles replaced with lexical handles
- CORE_version uses wantarray for scalar/list context return

Tests: 42 tests, all pass (including UI::Mojo and POPFile::Loader smoke)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…+ POD

Replaced all $self->{field__} hash-style accesses with proper Object::Pad
field declarations. Stripped __ and _ suffixes from all field names.
Added POD documentation for public methods.

Modules converted:
- POPFile::Module (field declarations, no more BUILD hash assignments)
- POPFile::Mutex ($name__/$locked__ → $lock_path/$locked)
- POPFile::Logger (5 fields, POD for all public methods)
- POPFile::MQ (5 fields including %queue/%waiters/%children)
- POPFile::Configuration (7 fields, accessor methods added for tests)
- POPFile::Loader (%components + 12 scalar fields)
- Services::Classifier (3 fields: $classifier/$history/$session)
- UI::Mojo ($service/$child_pid, run_server__ → run_server)
- Proxy::Proxy (9 fields, accessor methods for subclass init)
- Proxy::POP3/SMTP/NNTP (BUILD uses parent accessors, own fields declared)

Also updated t/lib/TestHelper.pm and t/configuration.t to use
accessor methods instead of direct hash access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… IMAP naming

- Classifier::MailParse: replace BUILD hash with field declarations, extract
  %color_map as module-level lexical, expose words/lang/mangle/color_resolver
  accessors used by Bayes
- Classifier::Bayes: replace direct parser hash access with accessor calls
- POPFile::History: replace BUILD hash assignments with field declarations + POD
- UI::HTTP, UI::XMLRPC: full Object::Pad conversion (class/field/method) + POD
- Services::IMAP, IMAP::Client: drop __ suffix from internal hash keys
- t/mailparse.t: use words() accessor instead of words__ hash slot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Module::log_ and Loader::CORE_warning/CORE_die were calling ->debug on
the logger before it was injected (field defaults to 0 / undef).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moved to legacy/UI/ (gitignored) for reference.
UI::Mojo + Svelte SPA covers all functionality.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port 110 requires root on Unix. 1110 is the conventional unprivileged
alternative for development/proxy use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POPFile::Logger: add BUILD { name('logger') } — was registered as '' in components
- Services::Classifier: move LOADABLE MODULE marker to line 1 so Loader picks it up
- UI::Mojo: fix get_root_path_() call to pass static_dir as argument
- cpanfile: promote SSL/SOCKS/DNS/EV/JSON deps from recommends to requires
- Proxy::POP3: default port 110→1110 (unprivileged)
- POPFile::Loader: guard CORE_warning/CORE_die against undef logger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Services::Classifier::start() creates the admin session used by
UI::Mojo routes. Previously it started after interface (alphabetic
sort), so the forked Mojo child had an empty session and all
classifier calls failed.

Also saves a memory note about Object::Pad sibling-class segfault.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New POPFile::Role::Logging: Object::Pad role providing log_($level,
  $message) via Log::Any::get_logger(category => ref($self))
- New POPFile::Log::Adapter: custom Log::Any adapter with file/stdout
  output, USER/PASS obscuring, ring buffer, popfile-level mapping
- POPFile::Module: apply Logging role, remove $logger field/accessor,
  remove log_/last_ten_log_entries; no more logger injection at startup
- POPFile::Logger: now a lifecycle module that configures the Log::Any
  adapter at start(); keeps TICKD/log-rotation logic
- POPFile::Loader: remove logger injection loop; CORE_warning/CORE_die
  use Log::Any directly
- cpanfile: add Log::Any >= 1.7
- TestHelper: remove stub Logger, update setup/wire/make_module signatures
- t/00-load.t: remove UI::HTTP (moved to legacy/)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Services::IMAP: full rewrite as Object::Pad class :isa(POPFile::Module)
  - Fields replace hash slots (%folders, @Mailboxes, $classifier, etc.)
  - BUILD sets name('imap'); initialize/start/stop/service as methods
  - Fix broken $self->debug($all, as => 'all') → $self->log_(1, $all)
  - Fix new_imap_client: remove dead $self->{logger__} ref; wire Client
    via configuration/mq/name instead of passing closures
  - Remove legacy HTML UI methods (configure_item, validate_item, etc.)
  - Qualifier for Fcntl constants (O_RDWR/O_CREAT) — Object::Pad class
    scope does not inherit file-level exports

- Services::IMAP::Client: full rewrite as Object::Pad class :isa(POPFile::Module)
  - Fields: $socket, $folder, $tag, $last_response, $last_command
  - All methods converted from sub to method
  - Reads config/global_config via inherited Module methods (no closures)
  - Uses inherited slurp_/log_ from POPFile::Module
  - Qualifies Socket::AF_INET() and Carp::confess() — same export scope issue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ields (Phase 12)

Convert all hand-written getter/setter methods of the form
  method foo ($val = undef) { $foo = $val if defined $val; return $foo }
to Object::Pad field attributes wherever the name collision with lifecycle
methods allows it.  Where a collision exists ($service vs service(),
$childexit vs childexit()) the manual method is kept.

- POPFile::Module: :reader :writer on $configuration, $mq, $name, $alive,
  $forker, $pipeready, $version; setchildexit() kept (lifecycle clash)
- POPFile::Configuration: :reader :writer on $popfile_root, $popfile_user,
  $started, $save_needed
- POPFile::History, Services::IMAP, Services::Classifier: :writer on
  $classifier / $history (set_classifier / set_history)
- Proxy::Proxy: :reader :writer on $child, error strings, $connect_banner;
  set_service() kept as combined getter/setter (lifecycle clash with service())
- Classifier::MailParse: :reader :writer on $mangle, $lang, $color_resolver
- Classifier::Bayes: history() setter renamed to set_history()
- Loader, TestHelper, proxy BUILD blocks, Bayes, tests: all call sites
  updated to set_* naming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All $self->{field__} hash accesses replaced with lexical Object::Pad
field declarations.  The BUILD block is reduced to set_name('bayes');
field defaults are declared inline.

- Scalar fields: wordscores__ (:reader :writer), wmformat__ (:reader :writer),
  db__ (:reader(db)), hostname__, history__, all db_* prepared statement
  handles, corpus_version__, unclassified__, magnet_used__, magnet_detail__,
  db_is_sqlite__, db_name__, get_wordids__, db_classify__, db_getwords__,
  db_get_userid__
- Hash-ref fields (kept as scalars): db_bucketid__, db_parameterid__,
  db_parameters__, bucket_start__, not_likely__, api_sessions__,
  db_bucketcount__, db_bucketunique__, possible_colors__
- MailParse object: field $parser__ = Classifier::MailParse->new()
- Removed manual wordscores/wmformat/db accessor methods; kept set_history
- Added parser__() method for external callers (History, Loader, tests)
- Fixed residual $self->{alive_} -> $self->alive() and
  $self->{configuration__} -> $self->configuration()
- Updated History.pm, Loader.pm and test to use ->parser__() method
- Updated bayes-classify.t to use public API instead of hash introspection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e 14)

- Remove vertical alignment from all field declarations
- Convert #--- comment blocks before methods to =head2 POD
- Remove leading blank lines after opening { and trailing blank before }

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Object::Pad lexical fields provide privacy without naming conventions.
Renamed all $field__ → $field and method__ → method throughout
Classifier/Bayes.pm; updated external parser__ callers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These were markers for a Perl profiler (NYTProf-style) that forced
artificial line splits. Nothing in the codebase invokes that profiler
and the tags fought the formatting cleanup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Phase 17)

Create POPFile::Role::Loadable carrying the ten lifecycle method defaults
(previously stubs in Module.pm). Module.pm composes the role, so all
existing subclasses inherit it transparently.

Loader now verifies DOES('POPFile::Loadable') after instantiation instead
of reading and pattern-matching the first line of each .pm file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renames all underscore-suffixed private/protected methods to clean names
across all project PM files, the three script files, and every call site:

- log_ → log_msg (avoids conflict with Perl built-in log())
- child__ → child (Proxy SMTP/POP3/NNTP, override of Proxy::Proxy base)
- echo_to_dot_ → echo_to_dot (both Proxy::Proxy and Classifier::Bayes)
- All other _ and __ suffixes stripped (config_, global_config_,
  module_config_, mq_post_, mq_register_, register_configuration_item_,
  get_user_path_, get_root_path_, slurp_, slurp_buffer_, done_slurp_,
  flush_extra_, yield_, read_pipe_, flush_child_data_, db__, commit_history__,
  make_directory__, upgrade_history_files__, history_read_class__,
  copy_file__, live_check_, check_pid_, get_pid_, write_pid_, delete_pid_,
  upgrade_parameter__, path_join__, get_color__, get_not_likely_, get_value_,
  get_base_value_, set_value_, get_sort_value_, tee_, echo_to_regexp_,
  get_response_, echo_response_, verify_connected_, smtp_echo_response_,
  get_message_id_, load_module_, root_path__, and all IMAP-service methods)

All 41 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename all methods with trailing _ (private) and __ (protected) suffixes
across project PM files. log_ renamed to log_msg to avoid conflict with
Perl built-in log(). All 41 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jan Limpens and others added 11 commits March 25, 2026 08:16
Unjam multi-statement lines left by prior alignment-removal phases across
Bayes.pm, MailParse.pm, Configuration.pm, History.pm, and MQ.pm. Also
convert string lists to qw(), use fat comma for key-value pairs, and
remove restatement comments throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TestHelper: add setup_bayes, configure_db, reset_db, load_fixture helpers
- TestHelper: inject History stub and in-memory SQLite for Classifier::Bayes
- Bayes: fix stop() to reinitialize parser and clear caches (enables restart)
- New tests: bayes-magnets.t, bayes-fixtures.t
- New fixture: t/fixtures/two-buckets-trained.pl

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (UI/Mojo.pm):
- Register password, local, page_size, date_format, session_dividers,
  wordtable_format in initialize()
- Replace 4-key config list with full %CFG map covering all modules:
  UI, POP3, SMTP, NNTP, Classifier/Bayes, History, Logger
- Fix config read/write to use module_config() instead of global_config()
- Save config to popfile.cfg after writes

Frontend:
- App.svelte: CSS custom properties for dark/light themes (persisted to
  localStorage), sticky nav with bottom-border active indicator,
  theme toggle button, global table/form/heading styles
- Settings.svelte: VSCode/Zed-style two-pane settings page — sidebar
  with 8 sections (UI, Security, POP3, SMTP, NNTP, Classifier, History,
  Logging), per-row field/description layout, toggle switches for booleans,
  select dropdowns, Save Changes button with inline saved/error feedback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add IMAP.svelte: connection settings, watched folders editor,
  bucket→folder mappings table, training mode toggle
- Add Settings.svelte: VSCode/Zed-style two-pane settings layout
  with 8 sections (UI, Security, POP3, SMTP, NNTP, Classifier,
  History, Logging), toggle switches, selects, per-section save
- Update App.svelte: dark/light theme toggle with localStorage,
  CSS custom property theme system, add IMAP to nav
- Expand UI/Mojo.pm: %CFG map covering 40+ config params across
  all modules, IMAP folder API endpoints (GET/PUT /api/v1/imap/folders)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Loader: wrap class->new() in eval to skip non-loadable modules
  with required constructor args (e.g. POPFile::Mutex)
- Loader: use set_classifier/set_history instead of renamed methods
- Bayes: guard db_disconnect against uninitialized statement handles
- Logger: fix Log::Any::Adapter class name (needs + prefix)
- Logger: restore debug($level, $msg) method used by popfile.pl

https://claude.ai/code/session_018ysXkkvRRCcPRZB1cuJ4ki
lib/Config.pm, lib/Config_heavy.pl, lib/lib.pm were perl 5.8.9 build
artefacts that got copied into blib/ by make, causing version mismatch
errors on newer perls.

CI now uses carton instead of Makefile.PL/make.

https://claude.ai/code/session_018ysXkkvRRCcPRZB1cuJ4ki
cpanfile.snapshot is incomplete (macOS-generated), causing carton
--deployment to fail on missing distributions. cpanm resolves deps
directly from CPAN without needing a snapshot.

https://claude.ai/code/session_018ysXkkvRRCcPRZB1cuJ4ki
- prove not in PATH with shogo82148/actions-setup-perl; use App::Prove
- add -I. to perl -c so intra-repo modules can be found

https://claude.ai/code/session_018ysXkkvRRCcPRZB1cuJ4ki
@janlimpens janlimpens merged commit 89a2362 into main Mar 25, 2026
2 checks passed
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.

2 participants