@mailwoman/neural-weights-fr-fr 7.4.0 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mailwoman/neural-weights-fr-fr",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "description": "Mailwoman neural-classifier weights for locale 'fr-fr'. Data-only overlay — shares the base model.onnx + tokenizer.model from @mailwoman/neural-weights-en-us (they are byte-identical); ships only the fr-specific data siblings. Loaded by @mailwoman/neural at runtime.",
5
5
  "license": "AGPL-3.0-only OR LicenseRef-Commercial",
6
6
  "repository": {
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@mailwoman/neural-weights-en-us": "7.4.0"
30
+ "@mailwoman/neural-weights-en-us": "7.6.0"
31
31
  },
32
32
  "mailwoman": {
33
33
  "baseWeights": "@mailwoman/neural-weights-en-us"
@@ -4,42 +4,37 @@
4
4
  * @license AGPL-3.0
5
5
  * @author Teffen Ellis, et al.
6
6
  *
7
- * Symlink dev model + tokenizer files into this package for local testing.
8
- * See @mailwoman/neural-weights-en-us/scripts/link-dev-weights.ts for the rationale.
7
+ * Materialize the fr-fr overlay's dev artifacts. See
8
+ * @mailwoman/neural-weights-en-us/scripts/link-dev-weights.ts for the base rationale.
9
9
  *
10
- * A single multilingual model serves both en-us and fr-fr (byte-identical artifact;
11
- * fr-fr just carries its own calibration). Re-symlinks the SAME files as en-us until
12
- * per-locale training lands. Keep these defaults in lockstep with en-us's DEFAULT_*
13
- * on every ship (currently v5.1.0 = bsplice-meaninit + the spliced v0.6.0-bsplice
14
- * tokenizer). The md5 guard reads en-us's model-card `files_md5` — one truth for the
15
- * one artifact (fr-fr's own card carries no files_md5 block).
10
+ * #1179 OVERLAY FORM (2026-07-23 rewrite): fr-fr declares `mailwoman.baseWeights:
11
+ * "@mailwoman/neural-weights-en-us"`, so `resolveWeights` falls through to the en-us package
12
+ * for `model.onnx` / `tokenizer.model` / the card. This script therefore no longer links a
13
+ * model or tokenizer at all — it REMOVES any leftover local pair so the base fallback engages.
14
+ * (The previous version re-symlinked a pinned v241-fr-nsplice model on every `yarn test`;
15
+ * since #1179 that local file SHADOWED the base fallback, silently running the stale model for
16
+ * every dev fr-fr parse, and its #397 md5 guard could never pass against the en-us card. One
17
+ * model, one pin — en-us's script owns it.)
18
+ *
19
+ * What fr-fr DOES own locally (locale-specific soft-feed siblings; `resolveFromPackageDir`
20
+ * resolves these from the overlay dir with no base fallback):
21
+ *
22
+ * - `anchor-lexicon-v1.json` / `country-surface-lexicon-v1.json` — checked-in repo files,
23
+ * symlinked from `data/gazetteer/`.
24
+ * - `postcode-fr.bin` — derived from the WOF intl postcode shard
25
+ * (`softFeed.postcodeDBByCountry.fr` = postalcode-intl.db), built in place via the compiled
26
+ * `gazetteer postcode-binary` CLI (skip-if-exists; rebuilds in seconds). Without it a fresh
27
+ * worktree parses anchor-OFF — see the en-us script's section comment for the CI failure
28
+ * this caused.
16
29
  */
17
30
 
18
- import { createHash } from "node:crypto"
19
- import { existsSync, readFileSync, symlinkSync, unlinkSync } from "node:fs"
31
+ import { spawnSync } from "node:child_process"
32
+ import { existsSync, lstatSync, symlinkSync, unlinkSync } from "node:fs"
20
33
  import { resolve } from "node:path"
21
34
 
22
- import { $public } from "@mailwoman/core/env"
23
35
  import { dataRootPath, repoRootPath } from "@mailwoman/core/utils"
24
36
 
25
37
  const PKG_DIR = repoRootPath("neural-weights-fr-fr")
26
- // In lockstep with en-us's DEFAULT_* (one multilingual artifact serves both) — v5.9.0
27
- // v241-fr-nsplice-ft pair. The 2026-07-02 ship missed this bump (both linkers still pinned
28
- // the demo-only v4.16.0 pair); the guard below now fails loud on any future miss.
29
- const SRC_MODEL =
30
- $public.MAILWOMAN_DEV_MODEL || dataRootPath("models", "quantized", "model-v241-fr-nsplice-ft-step-12000-int8.onnx")
31
- const SRC_TOKENIZER =
32
- $public.MAILWOMAN_DEV_TOKENIZER || dataRootPath("models", "tokenizer", "v0.8.0-fr-nsplice", "tokenizer.model")
33
-
34
- if (!existsSync(SRC_MODEL)) {
35
- console.error(`missing source model: ${SRC_MODEL}`)
36
- process.exit(1)
37
- }
38
-
39
- if (!existsSync(SRC_TOKENIZER)) {
40
- console.error(`missing source tokenizer: ${SRC_TOKENIZER}`)
41
- process.exit(1)
42
- }
43
38
 
44
39
  /** Replicate `ln -sf SRC DEST`: drop any pre-existing link/file at the destination, then symlink. */
45
40
  function linkForce(src: string, dest: string): void {
@@ -50,44 +45,62 @@ function linkForce(src: string, dest: string): void {
50
45
  symlinkSync(src, dest)
51
46
  }
52
47
 
53
- linkForce(SRC_MODEL, resolve(PKG_DIR, "model.onnx"))
54
- linkForce(SRC_TOKENIZER, resolve(PKG_DIR, "tokenizer.model"))
48
+ /** Remove a leftover local file/symlink so the #1179 base-weights fallback engages. */
49
+ function removeIfPresent(dest: string): void {
50
+ try {
51
+ lstatSync(dest)
52
+ } catch {
53
+ return
54
+ }
55
+ unlinkSync(dest)
56
+ console.log(`removed stale local ${dest} (base fallback to en-us engages)`)
57
+ }
58
+
59
+ removeIfPresent(resolve(PKG_DIR, "model.onnx"))
60
+ removeIfPresent(resolve(PKG_DIR, "tokenizer.model"))
55
61
 
56
- console.log(`linked ${PKG_DIR}/{model.onnx,tokenizer.model}`)
62
+ // --- soft-feed siblings (locale-owned; the fresh-worktree anchor-OFF gap) ----------------
63
+ const SRC_GAZETTEER_LEXICON = repoRootPath("data", "gazetteer", "anchor-lexicon-v1.json")
64
+ const SRC_COUNTRY_LEXICON = repoRootPath("data", "gazetteer", "country-surface-lexicon-v1.json")
57
65
 
58
- // #397 guard, lockstep form: the fr-fr artifact IS the en-us artifact, so verify the
59
- // linked default bytes against en-us's model-card `files_md5` (skipped under an
60
- // explicit MAILWOMAN_DEV_* override — deliberate experimentation).
61
- if (!$public.MAILWOMAN_DEV_MODEL || !$public.MAILWOMAN_DEV_TOKENIZER) {
62
- const enUSCard = JSON.parse(
63
- readFileSync(resolve(PKG_DIR, "..", "neural-weights-en-us", "model-card.json"), "utf8")
64
- ) as { files_md5?: Record<string, string> }
65
- const checks: Array<[string, string, string | undefined]> = [
66
- ["model", resolve(PKG_DIR, "model.onnx"), enUSCard.files_md5?.["model.onnx"]],
67
- ["tokenizer", resolve(PKG_DIR, "tokenizer.model"), enUSCard.files_md5?.["tokenizer.model"]],
68
- ]
66
+ if (existsSync(SRC_GAZETTEER_LEXICON)) {
67
+ linkForce(SRC_GAZETTEER_LEXICON, resolve(PKG_DIR, "anchor-lexicon-v1.json"))
68
+ console.log(`linked ${PKG_DIR}/anchor-lexicon-v1.json`)
69
+ } else {
70
+ console.error(`WARNING: missing ${SRC_GAZETTEER_LEXICON} — gazetteer channel will resolve OFF in this worktree.`)
71
+ }
72
+
73
+ if (existsSync(SRC_COUNTRY_LEXICON)) {
74
+ linkForce(SRC_COUNTRY_LEXICON, resolve(PKG_DIR, "country-surface-lexicon-v1.json"))
75
+ console.log(`linked ${PKG_DIR}/country-surface-lexicon-v1.json`)
76
+ } else {
77
+ console.error(`WARNING: missing ${SRC_COUNTRY_LEXICON} — country channel will resolve OFF in this worktree.`)
78
+ }
69
79
 
70
- for (const [label, path, expected] of checks) {
71
- if (
72
- ($public.MAILWOMAN_DEV_MODEL && label === "model") ||
73
- ($public.MAILWOMAN_DEV_TOKENIZER && label === "tokenizer")
74
- )
75
- continue
80
+ const FR_WOF_DB = dataRootPath("wof", "postalcode-intl.db")
81
+ const CLI = repoRootPath("mailwoman", "out", "cli.js")
82
+ const POSTCODE_BIN_DEST = resolve(PKG_DIR, "postcode-fr.bin")
76
83
 
77
- if (!expected) {
78
- console.error(
79
- `ERROR (#397 guard): en-us model-card.json has no files_md5 entry for ${label} — cannot verify the dev pin.`
80
- )
81
- process.exit(1)
82
- }
83
- const actual = createHash("md5").update(readFileSync(path)).digest("hex")
84
+ if (existsSync(POSTCODE_BIN_DEST)) {
85
+ console.log(`skipped postcode-fr.bin build — ${POSTCODE_BIN_DEST} already present`)
86
+ } else if (!existsSync(CLI)) {
87
+ console.error(
88
+ `WARNING: ${CLI} not built — run \`yarn compile\` first, then re-run this script to build postcode-fr.bin.`
89
+ )
90
+ } else if (!existsSync(FR_WOF_DB)) {
91
+ console.error(
92
+ `WARNING: missing ${FR_WOF_DB} — postcode-fr.bin not built; the anchor channel will resolve OFF for FR.`
93
+ )
94
+ } else {
95
+ const result = spawnSync(
96
+ process.execPath,
97
+ [CLI, "gazetteer", "postcode-binary", "--out", PKG_DIR, "--locale", `FR:${FR_WOF_DB}`],
98
+ { stdio: "inherit" }
99
+ )
84
100
 
85
- if (actual !== expected) {
86
- console.error(
87
- `ERROR (#397 guard): linked default ${label} md5 ${actual} != shipped ${expected} (en-us card files_md5).`
88
- )
89
- console.error(" Bump this script's SRC_* defaults in lockstep with en-us on each ship.")
90
- process.exit(1)
91
- }
101
+ if (result.status !== 0 || !existsSync(POSTCODE_BIN_DEST)) {
102
+ console.error(`ERROR: failed to build ${POSTCODE_BIN_DEST} (exit ${result.status})`)
103
+ process.exit(1)
92
104
  }
105
+ console.log(`built ${POSTCODE_BIN_DEST}`)
93
106
  }