Phase 0 — Foundation
Goal: lock the contracts (schema, classifier interface, policy, localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for.) before writing any modelneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' code. Refactor existing rule classifiers to use the new interface with zero behavior change.
Duration estimate: 1 week.
Branch: neural/phase-0-foundation
Pre-flight checks
- Mailwoman main branch checked out at latest commit
-
npm testpasses on a clean clone - You have read
README.md,reference/CONTEXT.md,reference/ARCHITECTURE.md,reference/SCHEMA.md,reference/INTERFACES.md,reference/OPERATIONS.md
Tasks
1. Monorepo restructure
Current Mailwoman is a single TypeScript project. Migrate to a workspaces layout.
- Set up
pnpmworkspaces (preferred) or yarn workspaces (Mailwoman currently uses yarn — verify and stick with it). - Create
packages/core/and move existingcore/,utils/,commands/,solvers/,filters/into it. Keep import paths working via package.jsonexports. - Create
packages/classifiers/and move existingclassifiers/into it. Depends on@mailwoman/core. - Update root
package.json,tsconfig.json,vitest.config.tsto be workspace-aware. - Verify
npm teststill passes. No behavior change yet.
Success: all existing tests pass, cli.ts still works, sdk/ still exports the same surface.
2. Schema implementation
Implement reference/SCHEMA.md in code.
- Create
packages/core/src/types/component.tswithCOMPONENT_TAGS,ComponentTag,BIO_LABELS,BioLabelas specified. - Add unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise. tests in
packages/core/test/component.test.ts:- Every tag appears exactly once in
COMPONENT_TAGS BIO_LABELShas correct length (1 + 2 × tags)BioLabeltype narrows correctly
- Every tag appears exactly once in
- Export from
packages/core/src/index.ts.
Success: schema is the single source of truth, importable as import { ComponentTag } from '@mailwoman/core'.
3. ClassificationProposal and Classifier interfaces
Implement reference/INTERFACES.md.
- Create
packages/core/src/types/classifier.tswithClassificationProposal,Classifier,ClassifierContext. - Create
packages/classifiers/src/adapter.tswithwrapLegacyClassifier. - For every existing rule classifier in
packages/classifiers/src/, write a wrapper that adapts its output toClassificationProposal. Do not change the rule logic itself. - Add unitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise. tests verifying each wrapped classifier produces output equivalent to the pre-wrap output (modulo the new
sourceandsource_idfields).
Success: every rule classifier emits ClassificationProposal[]. The solver code is updated minimally to consume the new shape but produces identical solutions.
4. Policy registry
Implement ClassifierPolicy and PolicyRegistry.
- Create
packages/core/src/policy.tswith the types fromreference/INTERFACES.md. - Create
packages/core/src/policy-defaults.ts— a default policy table where every component isrule_only. - Wire the registry into the solver path: before solving, filter proposals through
PolicyRegistry.apply(). - UnitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise. tests: policy filtering is correct for each mode.
Success: policy registrypolicy registryThe per-component table that decides which classifier (rule or neural) has authority for each address component. The Ship-of-Theseus dial. exists, defaults to current behavior (rule-only everywhere), is the single place a future migration to neural will edit.
5. Locale profile
Implement LocaleProfile and LocaleRegistry.
- Create
packages/core/src/locale.tswith the types fromreference/INTERFACES.md. - Create
packages/core/src/locales/en-us.tsandpackages/core/src/locales/fr-fr.tswith initial profiles.en-US: all current US-applicable rule classifiers, componentscountry, region, locality, postcode, house_number, street, street_prefix, street_suffix, unit, venue, attention, po_box, intersection_a, intersection_b.fr-FR: rule classifiers that apply to FR (most of the universal ones), components above pluscedex, street_prefix_particle, dependent_locality.
- Wire localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. into
ClassifierContextand the classification loop. If no localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. is provided, all classifiers withlocales: ['*']plus all registered localeslocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for.' classifiers run (backward compat with current behavior). - UnitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise. tests: localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. filtering works, profiles register correctly.
Success: the system has a concept of localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for.. Default behavior is unchanged. Setting a localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for. narrows the active classifiers.
6. CLI flag passthrough
- Add
--locale <bcp47>flag tocli.ts. When set, passes through to classification. - UnitunitA subdivision of a building — apartment, suite, floor — that refines a street address. Mailwoman's unit component; a designator plus identifier forms a subpremise. test:
npx mailwoman parse --locale en-US "..."works.
7. Documentation
- Update root
README.mdwith a section on the new architecture. Brief, links to plan docs. - Update
docs/(if it exists) with the new component schema as a reference table.
8. Forward-compat sanity check
Before declaring PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 0 done, verify the design handles PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 6 (Japan) cleanly:
- Mentally add a
ja-JPLocaleProfilewithcomponentsSupported: ['prefecture', 'municipality', 'district', 'block', 'sub_block', 'building_number', 'building_name', 'country', 'postcode'](nostreet, nohouse_number). - Does any core code break? It shouldn't. If it does, the abstraction is wrong — fix it now, not in PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 6.
- Verify by running the test suite with a hypothetical JP profile registered (you don't need actual JP classifiers; just verify the registration doesn't throw and the type system is happy).
Success criteria checklist
Before tagging neural-phase-0-complete:
-
npm testpasses across all workspaces -
npm run lintclean -
npx mailwoman parse "Mt Tabor Park, 6220 SE Salmon St, Portland, OR 97215, USA"produces output equivalent to pre-refactor -
npx mailwoman parse --locale fr-FR "8 rue de la République, 75008 Paris, France"runs without error (output quality not yet judged — that's PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 2+) - Forward-compat JP check passes
-
LOG.mdhas entries for each task -
DECISIONS.mdrecords any non-obvious choices - Branch is merged to main, tagged
Things that look like Phase 0 but aren't
These are tempting in PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 0; resist:
- ❌ Writing any neural code.
packages/neural/doesn't exist yet. - ❌ Building any corpuscorpusThe BIO-labeled training data used to train Mailwoman's neural classifier. Assembled from real sources (OpenAddresses, National Address Database) and synthetic shards (boundary stress, order variants, negative space). Managed by @mailwoman/corpus. adapters.
packages/corpus/doesn't exist yet. - ❌ Adding new rule classifiers for FR-specific concepts. That's Phase 1 work, after corpuscorpusThe BIO-labeled training data used to train Mailwoman's neural classifier. Assembled from real sources (OpenAddresses, National Address Database) and synthetic shards (boundary stress, order variants, negative space). Managed by @mailwoman/corpus. exists to validate against.
- ❌ Optimizing the solver. Don't change behavior in PhasephaseA milestone in the implementation plan (Foundation, Corpus, Training, Integration, and forward-looking phases). Distinct from stage (runtime pipeline) and tier (model vocabulary). 0; only refactor.
When to call this phase done
When you can demonstrate: "the system runs identically to before, but every classifier output flows through a typed, localelocaleThe combination of language and country an address comes from. en-US and fr-FR are the locales Mailwoman ships weights for.-aware, policy-gated pipelinestaged pipelineMailwoman's runtime architecture: a sequence of pure-function stages (normalize → query-shape → locale-gate → kind-classifier → phrase-grouper → classifier → decoder) connected by typed handoffs. Each stage is published as its own npm package. ready for a neural classifierneural classifierThe machine learning model at the core of Mailwoman's parser — a transformer encoder (~30M parameters) trained from scratch to do BIO token classification over addresses. It learns the 'grammar' of address formats; the gazetteer supplies the 'atlas.' to plug into."