@twardoch/namzy 1.0.19 → 1.0.23

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/dist/cli.js CHANGED
@@ -2,22 +2,20 @@
2
2
  // this_file: src/cli.ts
3
3
  import { generate } from "./index.js";
4
4
  const HELP = `
5
- namzy — generate fun fused project names
5
+ namzy — generate compact, memorable, unique names
6
6
 
7
7
  Usage:
8
8
  namzy [options]
9
9
 
10
10
  Options:
11
11
  --count <N> Number of names to generate (default: 1)
12
+ --seed <N> Integer seed (default: current timestamp)
12
13
  --help Show this help
13
-
14
- Examples:
15
- namzy
16
- namzy --count 5
17
14
  `.trimStart();
18
15
  function parseArgs(argv) {
19
16
  const args = argv.slice(2);
20
17
  let count = 1;
18
+ let seed;
21
19
  for (let i = 0; i < args.length; i++) {
22
20
  const arg = args[i];
23
21
  switch (arg) {
@@ -29,28 +27,33 @@ function parseArgs(argv) {
29
27
  case "--count": {
30
28
  const n = parseInt(args[++i], 10);
31
29
  if (Number.isNaN(n) || n < 1) {
32
- process.stderr.write(`--count must be a positive integer\n`);
30
+ process.stderr.write("--count must be a positive integer\n");
33
31
  process.exit(1);
34
32
  }
35
33
  count = n;
36
34
  break;
37
35
  }
36
+ case "--seed": {
37
+ const n = parseInt(args[++i], 10);
38
+ if (Number.isNaN(n)) {
39
+ process.stderr.write("--seed must be an integer\n");
40
+ process.exit(1);
41
+ }
42
+ seed = n;
43
+ break;
44
+ }
38
45
  default:
39
46
  process.stderr.write(`Unknown flag: ${arg}\nRun namzy --help for usage.\n`);
40
47
  process.exit(1);
41
48
  }
42
49
  }
43
- return { count };
50
+ return { count, seed };
44
51
  }
45
- async function main() {
46
- const { count } = parseArgs(process.argv);
47
- const base = Date.now();
52
+ function main() {
53
+ const { count, seed } = parseArgs(process.argv);
54
+ const base = seed ?? Date.now();
48
55
  for (let i = 0; i < count; i++) {
49
- const name = await generate({ seed: base + i * 1337 });
50
- process.stdout.write(`${name}\n`);
56
+ process.stdout.write(`${generate({ seed: base + i * 2654435761 })}\n`);
51
57
  }
52
58
  }
53
- main().catch((err) => {
54
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
55
- process.exit(1);
56
- });
59
+ main();
package/dist/index.d.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { activeRotationMask, joinClean, mangle, mulberry32 } from "./mangle.js";
1
+ import { applyRotation, buildName, mulberry32 } from "./mangle.js";
2
2
  export interface NamzyOptions {
3
3
  seed?: number;
4
4
  }
5
- /**
6
- * Generate a single fused, mangled namzy name.
7
- * Two raw words junction-cleaned fusion consonant rotation → capitalize.
8
- */
9
- export declare function generate(opts?: NamzyOptions): Promise<string>;
10
- export { COMMON, GEO } from "./wordlist.js";
11
- export { activeRotationMask, joinClean, mangle, mulberry32 };
5
+ /** Generate one namzy name. Default seed is the current timestamp. */
6
+ export declare function generate(opts?: NamzyOptions): string;
7
+ /** Generate `count` names. Distinct seeds derived from the base seed. */
8
+ export declare function generateMany(count: number, opts?: NamzyOptions): string[];
9
+ export { STEMS, ROTATIONS } from "./wordlist.js";
10
+ export { applyRotation, buildName, mulberry32 };
package/dist/index.js CHANGED
@@ -1,24 +1,18 @@
1
1
  // this_file: src/index.ts
2
- import { activeRotationMask, joinClean, mangle, mulberry32 } from "./mangle.js";
3
- import { COMMON, GEO } from "./wordlist.js";
4
- function capitalize(s) {
5
- return s.charAt(0).toUpperCase() + s.slice(1);
6
- }
7
- function pick(arr, rng) {
8
- return arr[Math.floor(rng() * arr.length)];
9
- }
10
- /**
11
- * Generate a single fused, mangled namzy name.
12
- * Two raw words → junction-cleaned fusion → consonant rotation → capitalize.
13
- */
14
- export async function generate(opts) {
2
+ import { applyRotation, buildName, mulberry32 } from "./mangle.js";
3
+ /** Generate one namzy name. Default seed is the current timestamp. */
4
+ export function generate(opts) {
15
5
  const seed = opts?.seed ?? Date.now();
16
- const rng = mulberry32(seed);
17
- const w1 = pick(GEO, rng).toLowerCase();
18
- const w2 = pick(COMMON, rng).toLowerCase();
19
- const [first, second] = rng() < 0.5 ? [w1, w2] : [w2, w1];
20
- const fused = joinClean(first, second);
21
- return capitalize(mangle(fused, activeRotationMask(rng)));
6
+ return buildName(mulberry32(seed));
7
+ }
8
+ /** Generate `count` names. Distinct seeds derived from the base seed. */
9
+ export function generateMany(count, opts) {
10
+ const base = opts?.seed ?? Date.now();
11
+ const out = [];
12
+ for (let i = 0; i < count; i++) {
13
+ out.push(generate({ seed: base + i * 2654435761 }));
14
+ }
15
+ return out;
22
16
  }
23
- export { COMMON, GEO } from "./wordlist.js";
24
- export { activeRotationMask, joinClean, mangle, mulberry32 };
17
+ export { STEMS, ROTATIONS } from "./wordlist.js";
18
+ export { applyRotation, buildName, mulberry32 };
package/dist/mangle.d.ts CHANGED
@@ -1,15 +1,5 @@
1
- /** Simple seeded mulberry32 RNG — returns a function that yields [0,1) floats */
2
1
  export declare function mulberry32(seed: number): () => number;
3
- type RotationRule = readonly [from: string, to: string];
4
- export declare const ROTATION_RULES: readonly RotationRule[];
5
- /** Choose 1..10 active consonant-rotation rules from the seeded RNG. */
6
- export declare function activeRotationMask(rng: () => number): number;
7
- /** Consonant rotation. Case-preserving. Defaults to all rules for direct helper use. */
8
- export declare function mangle(s: string, activeMask?: number): string;
9
- /**
10
- * Fuse two lowercase words at a clean junction.
11
- * Drops the first char of `b` while it duplicates the tail of `a`,
12
- * or while the seam is vowel-vowel. Max two trims.
13
- */
14
- export declare function joinClean(a: string, b: string): string;
15
- export {};
2
+ /** Apply one rotation at a randomly chosen matching position. */
3
+ export declare function applyRotation(s: string, rng: () => number): string;
4
+ /** Build one name with junction validation + one rotation. */
5
+ export declare function buildName(rng: () => number): string;
package/dist/mangle.js CHANGED
@@ -1,85 +1,76 @@
1
1
  // this_file: src/mangle.ts
2
- /** Simple seeded mulberry32 RNG returns a function that yields [0,1) floats */
2
+ import { BAD_SEAMS, ROTATIONS, STEMS } from "./wordlist.js";
3
+ const MAX_LEN = 12;
4
+ const MAX_TRIES = 8;
3
5
  export function mulberry32(seed) {
4
6
  let s = seed >>> 0;
5
7
  return () => {
6
- s += 0x6d2b79f5;
8
+ s = (s + 0x6d2b79f5) >>> 0;
7
9
  let t = Math.imul(s ^ (s >>> 15), 1 | s);
8
10
  t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
9
11
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
10
12
  };
11
13
  }
12
- export const ROTATION_RULES = [
13
- ["c", "q"],
14
- ["f", "v"],
15
- ["k", "c"],
16
- ["q", "k"],
17
- ["s", "z"],
18
- ["z", "s"],
19
- ["v", "f"],
20
- ["w", "u"],
21
- ["b", "p"],
22
- ["p", "b"],
23
- ];
24
- const ALL_ROTATIONS = (1 << ROTATION_RULES.length) - 1;
25
- function rotationFor(lower, activeMask) {
26
- for (let i = 0; i < ROTATION_RULES.length; i++) {
27
- if ((activeMask & (1 << i)) === 0) {
28
- continue;
29
- }
30
- const [from, to] = ROTATION_RULES[i];
31
- if (lower === from) {
32
- return to;
33
- }
14
+ function pick(arr, rng) {
15
+ return arr[Math.floor(rng() * arr.length)];
16
+ }
17
+ function hasTripleLetter(s) {
18
+ for (let i = 2; i < s.length; i++) {
19
+ if (s[i] === s[i - 1] && s[i] === s[i - 2])
20
+ return true;
34
21
  }
35
- return undefined;
22
+ return false;
36
23
  }
37
- /** Choose 1..10 active consonant-rotation rules from the seeded RNG. */
38
- export function activeRotationMask(rng) {
39
- const order = ROTATION_RULES.map((_, i) => i);
40
- const activeCount = 1 + Math.floor(rng() * ROTATION_RULES.length);
41
- for (let i = 0; i < activeCount; i++) {
42
- const swap = i + Math.floor(rng() * (order.length - i));
43
- [order[i], order[swap]] = [order[swap], order[i]];
24
+ function junctionUgly(compound, junction) {
25
+ // Inspect the 4-char window centered on the junction.
26
+ const start = Math.max(0, junction - 2);
27
+ const end = Math.min(compound.length, junction + 2);
28
+ const win = compound.slice(start, end);
29
+ for (const seam of BAD_SEAMS) {
30
+ if (win.includes(seam))
31
+ return true;
44
32
  }
45
- return order.slice(0, activeCount).reduce((mask, i) => mask | (1 << i), 0);
33
+ return false;
46
34
  }
47
- /** Consonant rotation. Case-preserving. Defaults to all rules for direct helper use. */
48
- export function mangle(s, activeMask = ALL_ROTATIONS) {
49
- let out = "";
50
- for (const ch of s) {
51
- const lower = ch.toLowerCase();
52
- const repl = rotationFor(lower, activeMask);
53
- if (repl === undefined) {
54
- out += ch;
55
- }
56
- else {
57
- out += ch === lower ? repl : repl.toUpperCase();
35
+ /** Apply one rotation at a randomly chosen matching position. */
36
+ export function applyRotation(s, rng) {
37
+ const matches = [];
38
+ for (let i = 0; i < s.length; i++) {
39
+ for (const [src, dst] of ROTATIONS) {
40
+ if (s.slice(i, i + src.length) === src) {
41
+ matches.push({ i, src, dst });
42
+ }
58
43
  }
59
44
  }
60
- return out;
45
+ if (matches.length === 0)
46
+ return s;
47
+ const m = matches[Math.floor(rng() * matches.length)];
48
+ return s.slice(0, m.i) + m.dst + s.slice(m.i + m.src.length);
61
49
  }
62
- const VOWELS = new Set(["a", "e", "i", "o", "u", "y"]);
63
- /**
64
- * Fuse two lowercase words at a clean junction.
65
- * Drops the first char of `b` while it duplicates the tail of `a`,
66
- * or while the seam is vowel-vowel. Max two trims.
67
- */
68
- export function joinClean(a, b) {
69
- const head = a;
70
- let tail = b;
71
- for (let i = 0; i < 2 && head.length > 0 && tail.length > 0; i++) {
72
- const last = head[head.length - 1].toLowerCase();
73
- const first = tail[0].toLowerCase();
74
- if (last === first) {
75
- tail = tail.slice(1);
50
+ function capitalize(s) {
51
+ return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
52
+ }
53
+ /** Build one name with junction validation + one rotation. */
54
+ export function buildName(rng) {
55
+ let best = "";
56
+ for (let attempt = 0; attempt < MAX_TRIES; attempt++) {
57
+ const a = pick(STEMS, rng);
58
+ const b = pick(STEMS, rng);
59
+ const compound = a + b;
60
+ if (compound.length > MAX_LEN) {
61
+ best = best || compound;
62
+ continue;
76
63
  }
77
- else if (VOWELS.has(last) && VOWELS.has(first)) {
78
- tail = tail.slice(1);
64
+ if (hasTripleLetter(compound)) {
65
+ best = best || compound;
66
+ continue;
79
67
  }
80
- else {
81
- break;
68
+ if (junctionUgly(compound, a.length)) {
69
+ best = best || compound;
70
+ continue;
82
71
  }
72
+ return capitalize(applyRotation(compound, rng));
83
73
  }
84
- return head + tail;
74
+ // All attempts had some flaw; rotate and ship the first candidate anyway.
75
+ return capitalize(applyRotation(best, rng));
85
76
  }
package/dist/web.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { generate, mangle, joinClean, mulberry32, GEO, COMMON } from "./index.js";
1
+ export { generate, generateMany, applyRotation, buildName, mulberry32, STEMS, ROTATIONS } from "./index.js";
2
2
  export type { NamzyOptions } from "./index.js";
package/dist/web.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // this_file: src/web.ts
2
2
  // Browser entry point — exposes namzy.generate() on window.namzy.
3
- export { generate, mangle, joinClean, mulberry32, GEO, COMMON } from "./index.js";
3
+ export { generate, generateMany, applyRotation, buildName, mulberry32, STEMS, ROTATIONS } from "./index.js";
@@ -1,2 +1,3 @@
1
- export declare const GEO: string[];
2
- export declare const COMMON: string[];
1
+ export declare const STEMS: readonly string[];
2
+ export declare const ROTATIONS: readonly (readonly [string, string])[];
3
+ export declare const BAD_SEAMS: readonly string[];