Add packaging and modernization foundation (Phase 0)#1
Merged
janlimpens merged 41 commits intomainfrom Mar 25, 2026
Merged
Conversation
- 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>
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Windows-specific pipe code and debug-only
use DDPCo-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com