Skip to main content

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 test passes 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 pnpm workspaces (preferred) or yarn workspaces (Mailwoman currently uses yarn — verify and stick with it).
  • Create packages/core/ and move existing core/, utils/, commands/, solvers/, filters/ into it. Keep import paths working via package.json exports.
  • Create packages/classifiers/ and move existing classifiers/ into it. Depends on @mailwoman/core.
  • Update root package.json, tsconfig.json, vitest.config.ts to be workspace-aware.
  • Verify npm test still 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.ts with COMPONENT_TAGS, ComponentTag, BIO_LABELS, BioLabel as 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_LABELS has correct length (1 + 2 × tags)
    • BioLabel type narrows correctly
  • 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.ts with ClassificationProposal, Classifier, ClassifierContext.
  • Create packages/classifiers/src/adapter.ts with wrapLegacyClassifier.
  • For every existing rule classifier in packages/classifiers/src/, write a wrapper that adapts its output to ClassificationProposal. 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 source and source_id fields).

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.ts with the types from reference/INTERFACES.md.
  • Create packages/core/src/policy-defaults.ts — a default policy table where every component is rule_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.ts with the types from reference/INTERFACES.md.
  • Create packages/core/src/locales/en-us.ts and packages/core/src/locales/fr-fr.ts with initial profiles.
    • en-US: all current US-applicable rule classifiers, components country, 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 plus cedex, 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 ClassifierContext and 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 with locales: ['*'] 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 to cli.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.md with 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-JP LocaleProfile with componentsSupported: ['prefecture', 'municipality', 'district', 'block', 'sub_block', 'building_number', 'building_name', 'country', 'postcode'] (no street, no house_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 test passes across all workspaces
  • npm run lint clean
  • 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.md has entries for each task
  • DECISIONS.md records 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."