@namingsignal/cli 0.2.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.
@@ -0,0 +1,402 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compileBrief = compileBrief;
4
+ exports.reconcileBriefUncertainties = reconcileBriefUncertainties;
5
+ const CHANNELS = [
6
+ ["developer agents", /\b(?:codex|claude code|cursor|mcp|developer agents?)\b/i],
7
+ ["word of mouth", /\b(?:word[- ]of[- ]mouth|referrals?|spoken|podcasts?)\b/i],
8
+ ["creator video", /\b(?:instagram|youtube|tiktok|reels|shorts|creator(?:s| marketing)?|ugc|video)\b/i],
9
+ ["app stores", /\b(?:app store|google play|aso|ios|android)\b/i],
10
+ ["search", /\b(?:seo|organic search|search traffic|content marketing)\b/i],
11
+ ["paid acquisition", /\b(?:paid ads?|performance marketing|paid acquisition)\b/i],
12
+ ["sales", /\b(?:sales-led|enterprise sales|outbound|sales team)\b/i],
13
+ ["communities", /\b(?:community|communities|product hunt|hacker news|reddit)\b/i],
14
+ ];
15
+ const TONES = [
16
+ "calm",
17
+ "capable",
18
+ "clear",
19
+ "rhythmic",
20
+ "memorable",
21
+ "trustworthy",
22
+ "developer-native",
23
+ "technical",
24
+ "friendly",
25
+ "premium",
26
+ "confident",
27
+ "playful",
28
+ "serious",
29
+ "private",
30
+ "bold",
31
+ "minimal",
32
+ "approachable",
33
+ "warm",
34
+ "crafted",
35
+ "elegant",
36
+ ];
37
+ const NAMING_GUIDANCE = /^(?:(?:the )?(?:name|brand|names)\s+(?:should|must|needs? to)|(?:name|brand)\s+guidance\b)/i;
38
+ const STANDARD_UNCERTAINTIES = {
39
+ product: "A meaningful product description could not be inferred; add a few descriptive words.",
40
+ primaryUser: "Primary user was not explicit; confirm who should love the first release.",
41
+ pain: "The current pain or fragmented workflow was not explicit.",
42
+ outcome: "The promised user outcome needs confirmation.",
43
+ mechanism: "The differentiating mechanism needs confirmation.",
44
+ channels: "Acquisition channel is unknown; spoken-name weighting may change.",
45
+ future: "Future product scope is unknown; confirm how broad the brand must remain.",
46
+ domains: "Preferred TLD and domain acquisition budget are unknown.",
47
+ tone: "Desired brand tone needs confirmation.",
48
+ };
49
+ const PLATFORMS = [
50
+ ["web", /\b(?:web(?:site| app)?|saas|browser|landing page)\b/i],
51
+ ["mobile", /\b(?:mobile|ios|android|app store)\b/i],
52
+ ["desktop", /\b(?:desktop|macos|windows|linux app)\b/i],
53
+ ["cli", /\b(?:cli|command line|terminal)\b/i],
54
+ ["api", /\bapi\b/i],
55
+ ["mcp", /\bmcp\b/i],
56
+ ["extension", /\b(?:browser|chrome|vscode) extension\b/i],
57
+ ];
58
+ const INJECTION_LINE = /\b(?:ignore.{0,48}(?:instructions?|prompts?)|system message|developer message|jailbreak|reveal (?:the )?(?:prompt|secret)|do not follow)\b/i;
59
+ const SECTION_FIELDS = [
60
+ ["product", /^(?:product|what it is|product description)$/i],
61
+ ["primaryUser", /^(?:primary user|target user|audience|customer|who it is for)$/i],
62
+ ["pain", /^(?:core )?(?:pain|problem|friction|current problem)$/i],
63
+ ["outcome", /^(?:outcome|promise|transformation|result|benefit)$/i],
64
+ ["mechanism", /^(?:mechanism(?: and differentiators)?|how it works|approach|unique mechanism|differentiators)$/i],
65
+ ["futureExpansion", /^(?:scope and expansion|future expansion|future scope|roadmap|long-term scope)$/i],
66
+ ["avoid", /^(?:avoid|do not use|names to avoid|banned roots|patterns to avoid)$/i],
67
+ ];
68
+ function markdownSections(raw) {
69
+ const sections = [];
70
+ let heading = "";
71
+ let body = [];
72
+ const flush = () => {
73
+ if (heading && body.some((line) => line.trim()))
74
+ sections.push({ heading, body: body.join("\n").trim() });
75
+ };
76
+ for (const line of raw.normalize("NFKC").replace(/\r\n?/g, "\n").split("\n")) {
77
+ const match = line.match(/^\s{0,3}#{2,6}\s+(.+?)\s*#*\s*$/);
78
+ if (match) {
79
+ flush();
80
+ heading = compact(match[1], 100);
81
+ body = [];
82
+ }
83
+ else if (heading) {
84
+ body.push(line);
85
+ }
86
+ }
87
+ flush();
88
+ return sections;
89
+ }
90
+ function sectionMap(raw) {
91
+ const mapped = {};
92
+ for (const section of markdownSections(raw)) {
93
+ const field = SECTION_FIELDS.find(([, pattern]) => pattern.test(section.heading))?.[0];
94
+ if (field && !mapped[field])
95
+ mapped[field] = section;
96
+ }
97
+ return mapped;
98
+ }
99
+ function sectionProse(section, maximum = 720) {
100
+ if (!section)
101
+ return "";
102
+ return compact(cleanMarkdown(section.body), maximum);
103
+ }
104
+ function sectionFirstSentence(section, maximum = 520) {
105
+ const prose = sectionProse(section, Math.max(maximum, 720));
106
+ return compact(usefulSentences(prose)[0] || prose, maximum);
107
+ }
108
+ function sectionBullets(section) {
109
+ if (!section)
110
+ return [];
111
+ const bullets = section.body
112
+ .split("\n")
113
+ .map((line) => line.match(/^\s*(?:[-*+] |\d+[.)] )(.+)$/)?.[1])
114
+ .map((line) => compact(line, 220))
115
+ .filter((line) => Boolean(line));
116
+ return [...new Set(bullets)].slice(0, 20);
117
+ }
118
+ function cleanMarkdown(input) {
119
+ return input
120
+ .normalize("NFKC")
121
+ .replace(/\r\n?/g, "\n")
122
+ .replace(/```[\s\S]*?```/g, " ")
123
+ .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
124
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
125
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
126
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
127
+ .replace(/^\s{0,3}(?:#{1,6}|>|[-*+]\s|\d+[.)]\s)\s*/gm, "")
128
+ .replace(/[*_`~]/g, "")
129
+ .replace(/[ \t]+/g, " ")
130
+ .replace(/\n{3,}/g, "\n\n")
131
+ .trim();
132
+ }
133
+ function compact(value, maximum = 280) {
134
+ if (!value)
135
+ return "";
136
+ const normalized = value.replace(/\s+/g, " ").trim();
137
+ if (normalized.length <= maximum)
138
+ return normalized;
139
+ return `${normalized.slice(0, maximum - 1).trimEnd()}…`;
140
+ }
141
+ function meaningfulPhrase(value, maximum = 280) {
142
+ const normalized = compact(value, maximum);
143
+ const terms = normalized.match(/[\p{L}\p{N}]+/gu) ?? [];
144
+ if (terms.join("").length < 3)
145
+ return "";
146
+ return normalized;
147
+ }
148
+ function explicitField(lines, labels) {
149
+ const labelPattern = labels.map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
150
+ const pattern = new RegExp(`^(?:${labelPattern})\\s*[:\\-–—]\\s*(.+)$`, "i");
151
+ for (const line of lines) {
152
+ const match = line.match(pattern);
153
+ if (match?.[1])
154
+ return compact(match[1]);
155
+ }
156
+ return "";
157
+ }
158
+ function usefulSentences(text) {
159
+ // Markdown commonly hard-wraps one sentence across several lines. Treat
160
+ // single line breaks as whitespace so extraction never returns fragments
161
+ // such as “Their problem is not a”. Preserve paragraph boundaries from
162
+ // imported pages so headings cannot bleed into the next paragraph.
163
+ return text
164
+ .replace(/\n{2,}/g, "\u2029")
165
+ .replace(/\n+/g, " ")
166
+ .split(/(?<=[.!?])\s+|\u2029/)
167
+ .map((line) => meaningfulPhrase(line, 720))
168
+ .filter((line) => line.length >= 3 && line.length <= 720 && !INJECTION_LINE.test(line));
169
+ }
170
+ function firstMatching(sentences, pattern, excluded = []) {
171
+ const excludedKeys = new Set(excluded.flatMap((value) => [compact(value), coreProductDescription(value)])
172
+ .map((value) => value.toLocaleLowerCase("en-US"))
173
+ .filter(Boolean));
174
+ const preferred = sentences.find((sentence) => pattern.test(sentence) &&
175
+ !excludedKeys.has(compact(sentence).toLocaleLowerCase("en-US")) &&
176
+ !excludedKeys.has(coreProductDescription(sentence).toLocaleLowerCase("en-US")));
177
+ return preferred ?? sentences.find((sentence) => pattern.test(sentence)) ?? "";
178
+ }
179
+ /** Keep implementation details from masquerading as the product's core job. */
180
+ function coreProductDescription(value) {
181
+ return compact(value
182
+ .replace(/,\s+(?:built|based|trained|powered)\s+(?:on|by|with)\b.+$/i, ".")
183
+ .replace(/\s+using\b.+$/i, "."), 420);
184
+ }
185
+ function extractAudience(sentences) {
186
+ for (const sentence of sentences) {
187
+ const match = sentence.match(/\b(?:built|made|designed|intended) for\s+(.+?)(?=\s+(?:who|that|to|when|so they)\b|[.;]|$)|\b(?:tool|app|product|platform|service|software|website|api|autopilot|coach|tracker)\s+for\s+(.+?)(?=\s+(?:who|that|to|when|so they|with)\b|[.;]|$)|\bhelps?\s+(.+?)(?=\s+(?:who|to|by|with|generate|create|build|find|check|manage|understand)\b|[.;]|$)|\b(?:make|build|create|design)\b.+?\bfor\s+(.+?)(?=\s+(?:who|that|to|when|so they)\b|[.;]|$)|\bwhen\s+((?:a|an)\s+.+?)(?=\s+(?:has|have|wants?|needs?)\b)/i);
188
+ const result = compact(match?.[1] || match?.[2] || match?.[3] || match?.[4] || match?.[5], 140);
189
+ if (result)
190
+ return result.replace(/^(?:a|an|the)\s+/i, "");
191
+ }
192
+ return "";
193
+ }
194
+ function workoutAutopilotSummary(text) {
195
+ const workout = /\b(?:workout|exercise|exerciser|reps?|sets?|pushups?|squats?|burpees?)\b/i.test(text);
196
+ const audio = /\b(?:audio(?:-first)?|out loud|voice|announces?|calls? every rep|podcast|music|face-down)\b/i.test(text);
197
+ const automation = [
198
+ /\bpace(?:s|d)?\b/i,
199
+ /\b(?:runs?|manag(?:e|es|ing))\s+(?:sets?|rests?|countdowns?|transitions?)\b/i,
200
+ /\b(?:logs?|records?)\b.+\b(?:automatically|result|workout|work)\b/i,
201
+ /\b(?:does not|doesn't|without)\s+(?:need(?:ing)? to\s+)?(?:think|look|watch|count|manage)/i,
202
+ ].filter((pattern) => pattern.test(text)).length;
203
+ if (!workout || automation < 2)
204
+ return {};
205
+ const explicitAutopilot = /\bshort[- ]workout autopilot\b/i.test(text);
206
+ return {
207
+ product: audio
208
+ ? "An audio-first short-workout autopilot."
209
+ : explicitAutopilot
210
+ ? "A short-workout autopilot."
211
+ : "A short-workout autopilot.",
212
+ pain: "The mental overhead of pacing, counting reps, managing sets and rests, and logging makes short workouts harder to start and finish.",
213
+ outcome: "Start and finish a useful short workout without watching, counting, pacing, managing rests, or logging it manually.",
214
+ mechanism: audio
215
+ ? "Calm audio paces reps, sets, rests, countdowns, and transitions while the user's podcast or music continues and the phone can stay face-down."
216
+ : "It carries the workout by setting the rhythm, calling each rep and transition, running rests, and recording the result.",
217
+ };
218
+ }
219
+ function splitList(input) {
220
+ if (!input)
221
+ return [];
222
+ return [...new Set(input.split(/[,;/]|\band\b/i).map((item) => compact(item, 80)).filter(Boolean))];
223
+ }
224
+ function extractJsonDescription(raw) {
225
+ const trimmed = raw.trim();
226
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
227
+ return {};
228
+ try {
229
+ const value = JSON.parse(trimmed);
230
+ return {
231
+ product: typeof value.description === "string" ? compact(value.description) : undefined,
232
+ name: typeof value.name === "string" ? compact(value.name, 80) : undefined,
233
+ };
234
+ }
235
+ catch {
236
+ return {};
237
+ }
238
+ }
239
+ function mergeBrief(base, overrides) {
240
+ if (!overrides)
241
+ return base;
242
+ return {
243
+ ...base,
244
+ ...overrides,
245
+ platform: overrides.platform ? [...overrides.platform] : base.platform,
246
+ acquisitionChannels: overrides.acquisitionChannels
247
+ ? [...overrides.acquisitionChannels]
248
+ : base.acquisitionChannels,
249
+ brandTone: overrides.brandTone ? [...overrides.brandTone] : base.brandTone,
250
+ domainRequirements: overrides.domainRequirements
251
+ ? [...overrides.domainRequirements]
252
+ : base.domainRequirements,
253
+ avoid: overrides.avoid ? [...overrides.avoid] : base.avoid,
254
+ uncertainties: overrides.uncertainties ? [...overrides.uncertainties] : base.uncertainties,
255
+ evidence: overrides.evidence ? [...overrides.evidence] : base.evidence,
256
+ };
257
+ }
258
+ /**
259
+ * Compile an editable brief without an AI call. Embedded instructions are parsed
260
+ * as project data; this function never executes or delegates them.
261
+ */
262
+ function compileBrief(input) {
263
+ const request = typeof input === "string" ? { text: input } : input;
264
+ if (!request || typeof request.text !== "string" || !request.text.trim()) {
265
+ throw new TypeError("Project context must be non-empty text.");
266
+ }
267
+ const source = request.source || "pasted context";
268
+ const raw = request.text;
269
+ const sections = sectionMap(raw);
270
+ const text = cleanMarkdown(raw);
271
+ const lines = text.split("\n").map((line) => compact(line)).filter(Boolean);
272
+ const sentences = usefulSentences(text);
273
+ const descriptiveSentences = sentences.filter((sentence) => !NAMING_GUIDANCE.test(sentence));
274
+ const json = extractJsonDescription(raw);
275
+ const workoutSummary = workoutAutopilotSummary(text);
276
+ const sectionProduct = sectionFirstSentence(sections.product, 420);
277
+ const labeledProduct = explicitField(lines, ["product", "project", "what it is", "description"]);
278
+ const firstTitle = meaningfulPhrase(raw.match(/^[ \t]*#[ \t]+([^\r\n]+)$/m)?.[1], 160);
279
+ const product = coreProductDescription(sectionProduct ||
280
+ labeledProduct ||
281
+ json.product ||
282
+ workoutSummary.product ||
283
+ firstMatching(descriptiveSentences, /\b(?:tool|(?<!\.)app|service|platform|product|website|api|cli|saas|autopilot|coach|tracker)\b/i) ||
284
+ firstTitle ||
285
+ descriptiveSentences[0] ||
286
+ json.name ||
287
+ meaningfulPhrase(text));
288
+ const primaryUser = sectionFirstSentence(sections.primaryUser, 520) ||
289
+ explicitField(lines, ["primary user", "target user", "audience", "customer", "who it is for"]) ||
290
+ extractAudience(descriptiveSentences);
291
+ const pain = sectionProse(sections.pain, 720) ||
292
+ explicitField(lines, ["pain", "problem", "friction", "current problem"]) ||
293
+ workoutSummary.pain ||
294
+ firstMatching(descriptiveSentences, /\b(?:mental overhead|decision(?:s| cost)|offload|stuck|struggl\w*|friction|pain|problem|stress|wast\w*|spend\w*\s+\d+.+hours?|takes?\s+\d+.+hours?|generic\s+(?:ai\s+)?(?:tools?|output)|manual|fragmented|difficult|hard to|cannot|can't)\b/i, [product]);
295
+ const outcome = sectionFirstSentence(sections.outcome, 720) ||
296
+ explicitField(lines, ["outcome", "promise", "transformation", "result", "benefit"]) ||
297
+ workoutSummary.outcome ||
298
+ firstMatching(descriptiveSentences, /\b(?:helps?|enables?|lets?|allows?|so that|so the user|without (?:watching|managing|counting|looking)|finish(?:es)? with|turns? .+ into|result|(?:one|full|complete) week of|seven .+posts|week of .+\b(?:in|without)\b|(?:customers?|users?|teams?|people)\b.+?\b(?:get|receive|finish|can)|(?:puts?|adds?|syncs?)\b.+?\b(?:calendar|dashboard|workspace|inbox|timeline|schedule))\b/i, [product]);
299
+ const mechanism = sectionProse(sections.mechanism, 900) ||
300
+ explicitField(lines, ["mechanism", "how it works", "approach", "unique mechanism"]) ||
301
+ workoutSummary.mechanism ||
302
+ firstMatching(descriptiveSentences.filter((sentence) => !/\b(?:later|roadmap|future|expand\w*|eventually)\b/i.test(sentence)), /\b(?:one (?:upload|click|guided|weekly)|drop in|approve|review|regenerate|paces?|announces?|calls? every|runs? rests?|logs? automatically|mixes? over|reads?|checks?|analy[sz]es?|generates?|produces?|compares?|imports?|uses?|uploads?|scans?|extracts?|syncs?|schedules?|organizes?|summarizes?|transcribes?|puts?|cuts?|ships?|made from|built with|powered by)\b/i, [product, outcome]);
303
+ const platforms = PLATFORMS.filter(([, pattern]) => pattern.test(text)).map(([label]) => label);
304
+ const acquisitionChannels = CHANNELS.filter(([, pattern]) => pattern.test(text)).map(([label]) => label);
305
+ const labeledTone = splitList(explicitField(lines, ["tone", "brand tone", "voice", "brand voice"]));
306
+ const detectedTone = TONES.filter((tone) => new RegExp(`\\b${tone.replace("-", "[- ]")}\\b`, "i").test(text));
307
+ const brandTone = [...new Set([...labeledTone, ...detectedTone])].slice(0, 8);
308
+ const futureCandidates = descriptiveSentences.filter((sentence) => !/\b(?:future work should|read|consult)\b.+\b(?:first|before|start)\b/i.test(sentence) &&
309
+ !/\b(?:does not|not own|blur|never|avoid|anti-feature|microphone listening)\b/i.test(sentence));
310
+ const futureExpansion = sectionProse(sections.futureExpansion, 720) ||
311
+ explicitField(lines, ["future expansion", "future scope", "roadmap", "long-term scope"]) ||
312
+ firstMatching(futureCandidates, /\b(?:expand\w*|expansion|extension|long[- ]term|roadmap|beyond|multiple movement)\b/i) ||
313
+ firstMatching(futureCandidates, /\b(?:future (?:scope|versions?|sessions?|product|movement|features?)|later (?:versions?|expansion)|eventually (?:support|include|expand))\b/i);
314
+ const explicitDomains = splitList(explicitField(lines, ["domain", "domain requirements", "tld", "preferred tld", "acceptable tlds"]));
315
+ const mentionedTlds = [...new Set(text.match(/\.(?:com|io|dev|app|ai|co|net|org|sh|xyz)\b/gi) ?? [])].map((item) => item.toLowerCase());
316
+ const domainRequirements = [...new Set([...explicitDomains, ...mentionedTlds])];
317
+ const naturalAvoidSentence = firstMatching(sentences, /\b(?:want to avoid|avoid|do not use|don't use)\b/i);
318
+ const naturalAvoid = /^avoid saying\b/i.test(naturalAvoidSentence)
319
+ ? undefined
320
+ : naturalAvoidSentence.match(/\b(?:want to avoid|avoid|do not use|don't use)\s+(.+?)(?:[.!?]|$)/i)?.[1];
321
+ const namingGuidanceAvoid = sentences
322
+ .find((sentence) => NAMING_GUIDANCE.test(sentence))
323
+ ?.match(/\b(?:not|rather than)\s+(.+?)(?:[.!?]|$)/i)?.[1];
324
+ const sectionAvoid = sectionBullets(sections.avoid);
325
+ const avoid = sectionAvoid.length ? sectionAvoid : splitList(explicitField(lines, ["avoid", "do not use", "names to avoid", "banned roots", "patterns to avoid"]) || naturalAvoid || namingGuidanceAvoid || "");
326
+ const uncertainties = [];
327
+ if (!product)
328
+ uncertainties.push(STANDARD_UNCERTAINTIES.product);
329
+ if (!primaryUser)
330
+ uncertainties.push(STANDARD_UNCERTAINTIES.primaryUser);
331
+ if (!pain)
332
+ uncertainties.push(STANDARD_UNCERTAINTIES.pain);
333
+ if (!outcome)
334
+ uncertainties.push(STANDARD_UNCERTAINTIES.outcome);
335
+ if (!mechanism)
336
+ uncertainties.push(STANDARD_UNCERTAINTIES.mechanism);
337
+ if (!acquisitionChannels.length)
338
+ uncertainties.push(STANDARD_UNCERTAINTIES.channels);
339
+ if (!futureExpansion)
340
+ uncertainties.push(STANDARD_UNCERTAINTIES.future);
341
+ if (!domainRequirements.length)
342
+ uncertainties.push(STANDARD_UNCERTAINTIES.domains);
343
+ if (!brandTone.length)
344
+ uncertainties.push(STANDARD_UNCERTAINTIES.tone);
345
+ if (raw.split("\n").some((line) => INJECTION_LINE.test(line))) {
346
+ uncertainties.push("Instruction-like text was treated as quoted project data, not executed.");
347
+ }
348
+ const fieldValues = [
349
+ ["product", product],
350
+ ["primaryUser", primaryUser],
351
+ ["pain", pain],
352
+ ["outcome", outcome],
353
+ ["mechanism", mechanism],
354
+ ["futureExpansion", futureExpansion],
355
+ ];
356
+ const evidence = fieldValues
357
+ .filter(([, excerpt]) => Boolean(excerpt))
358
+ .slice(0, 8)
359
+ .map(([field, excerpt]) => ({ source, excerpt, field }));
360
+ if (sectionAvoid.length)
361
+ evidence.push({ source, excerpt: sectionAvoid.join("; "), field: "avoid" });
362
+ return reconcileBriefUncertainties(mergeBrief({
363
+ product,
364
+ primaryUser,
365
+ pain,
366
+ outcome,
367
+ mechanism,
368
+ platform: platforms,
369
+ acquisitionChannels,
370
+ brandTone,
371
+ futureExpansion,
372
+ domainRequirements,
373
+ avoid,
374
+ uncertainties,
375
+ evidence,
376
+ }, request.overrides));
377
+ }
378
+ /** Keep editable-brief uncertainty messages synchronized with current values. */
379
+ function reconcileBriefUncertainties(brief) {
380
+ const standard = new Set(Object.values(STANDARD_UNCERTAINTIES));
381
+ const preserved = (brief.uncertainties ?? []).filter((item) => !standard.has(item));
382
+ const uncertainties = [...preserved];
383
+ if (!brief.product.trim())
384
+ uncertainties.push(STANDARD_UNCERTAINTIES.product);
385
+ if (!brief.primaryUser.trim())
386
+ uncertainties.push(STANDARD_UNCERTAINTIES.primaryUser);
387
+ if (!brief.pain.trim())
388
+ uncertainties.push(STANDARD_UNCERTAINTIES.pain);
389
+ if (!brief.outcome.trim())
390
+ uncertainties.push(STANDARD_UNCERTAINTIES.outcome);
391
+ if (!brief.mechanism.trim())
392
+ uncertainties.push(STANDARD_UNCERTAINTIES.mechanism);
393
+ if (!brief.acquisitionChannels.length)
394
+ uncertainties.push(STANDARD_UNCERTAINTIES.channels);
395
+ if (!brief.futureExpansion.trim())
396
+ uncertainties.push(STANDARD_UNCERTAINTIES.future);
397
+ if (!brief.domainRequirements.length)
398
+ uncertainties.push(STANDARD_UNCERTAINTIES.domains);
399
+ if (!brief.brandTone.length)
400
+ uncertainties.push(STANDARD_UNCERTAINTIES.tone);
401
+ return { ...brief, uncertainties: [...new Set(uncertainties)] };
402
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sortCandidatesForDisplay = sortCandidatesForDisplay;
4
+ function dimensionValue(candidate, key) {
5
+ if (key === "overall")
6
+ return candidate.score?.overall;
7
+ if (key === "editorial")
8
+ return candidate.aiRank;
9
+ return candidate.score?.dimensions.find((dimension) => dimension.key === key)?.score;
10
+ }
11
+ function bestDomain(candidate, tlds) {
12
+ return tlds
13
+ .map((tld) => candidate.domains?.find((domain) => domain.domain.toLowerCase().endsWith(`.${tld}`)))
14
+ .find((domain) => domain?.status === "likely_available");
15
+ }
16
+ /** Sort exactly by the named public key. AI review is never hidden inside overall score order. */
17
+ function sortCandidatesForDisplay(candidates, key, tlds) {
18
+ return [...candidates].sort((left, right) => {
19
+ if (key === "editorial") {
20
+ if (left.aiRank !== undefined && right.aiRank === undefined)
21
+ return -1;
22
+ if (right.aiRank !== undefined && left.aiRank === undefined)
23
+ return 1;
24
+ if (left.aiRank !== undefined && right.aiRank !== undefined && left.aiRank !== right.aiRank) {
25
+ return left.aiRank - right.aiRank;
26
+ }
27
+ }
28
+ const leftValue = dimensionValue(left, key);
29
+ const rightValue = dimensionValue(right, key);
30
+ if (leftValue === undefined && rightValue !== undefined)
31
+ return 1;
32
+ if (rightValue === undefined && leftValue !== undefined)
33
+ return -1;
34
+ const selectedDelta = key === "editorial"
35
+ ? (leftValue ?? Number.MAX_SAFE_INTEGER) - (rightValue ?? Number.MAX_SAFE_INTEGER)
36
+ : (rightValue ?? 0) - (leftValue ?? 0);
37
+ if (selectedDelta)
38
+ return selectedDelta;
39
+ const leftTld = bestDomain(left, tlds)?.domain.split(".").pop();
40
+ const rightTld = bestDomain(right, tlds)?.domain.split(".").pop();
41
+ const tldDelta = (leftTld ? tlds.indexOf(leftTld) : tlds.length) - (rightTld ? tlds.indexOf(rightTld) : tlds.length);
42
+ if (tldDelta)
43
+ return tldDelta;
44
+ const overallDelta = (right.score?.overall ?? 0) - (left.score?.overall ?? 0);
45
+ return overallDelta || left.name.localeCompare(right.name);
46
+ });
47
+ }
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateFallbackCandidates = generateFallbackCandidates;
4
+ const brief_1 = require("./brief");
5
+ const normalize_1 = require("./normalize");
6
+ const brief_constraints_1 = require("./brief-constraints");
7
+ const STOP_WORDS = new Set("a an and are as at be because been but by can could did do does for from has have helps how i in into is it its let lets may more most must no not of on or our should so than that the their them then there these they this those through to tool use user users using very was we what when where which who will with without would you your project product service platform software website application app api cli mcp build built make makes help".split(" "));
8
+ const VEIN_PARTS = {
9
+ outcome: {
10
+ left: ["Clear", "Ready", "Open", "Bright", "Better", "Next", "Sure", "Right", "Good", "Fresh"],
11
+ right: ["Path", "Choice", "Signal", "Result", "Lift", "Start", "Fit", "Way", "Move", "Mark"],
12
+ rationale: "Frames the name around the result the user wants rather than the implementation.",
13
+ },
14
+ functional: {
15
+ left: ["Signal", "Context", "Pattern", "Project", "Source", "Intent", "Scope", "Brief", "Trace", "Field"],
16
+ right: ["Lens", "Scout", "Map", "Kit", "Forge", "Index", "Pilot", "Compass", "Lab", "Works"],
17
+ rationale: "Makes the product's working mechanism legible without becoming a feature list.",
18
+ },
19
+ metaphor: {
20
+ left: ["North", "Harbor", "Beacon", "Trail", "Bridge", "Lighthouse", "Anchor", "Cairn", "Orbit", "River"],
21
+ right: ["Star", "Line", "Mark", "Light", "Point", "Gate", "Stone", "Way", "Arc", "Rise"],
22
+ rationale: "Uses a familiar navigation or progress metaphor with room for a broader product story.",
23
+ },
24
+ trust: {
25
+ left: ["Proof", "True", "Known", "Sure", "Sound", "Verified", "Trust", "Solid", "Plain", "Honest"],
26
+ right: ["Ledger", "Signal", "Mark", "Record", "Check", "Source", "Ground", "Proof", "Base", "Guide"],
27
+ rationale: "Signals confidence and traceability while leaving legal or factual certainty to evidence.",
28
+ },
29
+ workflow: {
30
+ left: ["Name", "Launch", "Build", "Maker", "Project", "Decision", "Choice", "Shortlist", "Brand", "Venture"],
31
+ right: ["Desk", "Flow", "Loop", "Stack", "Board", "Lane", "Bench", "Foundry", "Dock", "Room"],
32
+ rationale: "Names the recurring builder workflow and can extend to adjacent decision tools.",
33
+ },
34
+ lateral: {
35
+ left: ["Field", "Relay", "Switch", "Thread", "Second", "Common", "Quiet", "Open", "Side", "Long"],
36
+ right: ["Note", "Yard", "Line", "Table", "Room", "House", "Run", "Form", "Arc", "Fold"],
37
+ rationale: "Moves outside category clichés to create a more ownable, memorable brand direction.",
38
+ },
39
+ coined: {
40
+ left: ["Noma", "Veri", "Sona", "Lumi", "Klar", "Meto", "Orbi", "Vela", "Aven", "Faro"],
41
+ right: ["lio", "via", "ora", "ary", "ivo", "una", "era", "ium", "aro", "ica"],
42
+ rationale: "Explores a compact coined form; pronunciation, language, and collision checks remain mandatory.",
43
+ },
44
+ };
45
+ const WORKOUT_VEIN_PARTS = {
46
+ functional: {
47
+ left: ["Tempo", "Pace", "Set", "Rep", "Cue", "Phase", "Rhythm", "Audio", "Motion", "Cadence"],
48
+ right: ["Guide", "Carry", "Steer", "Flow", "Run", "Pilot", "Path", "Voice", "Track", "Arc"],
49
+ rationale: "Makes the audio-guided workout mechanism legible without reducing the product to a timer.",
50
+ },
51
+ outcome: {
52
+ left: ["Done", "Ready", "Finish", "Steady", "Full", "Easy", "Clear", "Next", "Sure", "Calm"],
53
+ right: ["Flow", "Finish", "Resolve", "Carry", "Start", "Stride", "Motion", "Lift", "Run", "Way"],
54
+ rationale: "Centers momentum, completion, and relief from managing the workout.",
55
+ },
56
+ metaphor: {
57
+ left: ["Beacon", "Relay", "Compass", "Current", "Harbor", "Orbit", "Pulse", "Anchor", "Signal", "Trail"],
58
+ right: ["Beat", "Guide", "Arc", "Path", "Carry", "Line", "Light", "Run", "Mark", "Rise"],
59
+ rationale: "Uses a guidance or momentum metaphor that can extend across movement groups.",
60
+ },
61
+ lateral: {
62
+ left: ["Quiet", "Brio", "Drift", "Hush", "Open", "Soft", "True", "Side", "Daily", "Low"],
63
+ right: ["Loop", "Relay", "Arc", "Form", "Current", "Run", "Motion", "Fold", "Room", "Rise"],
64
+ rationale: "Creates distance from aggressive fitness and generic timer language while preserving calm motion.",
65
+ },
66
+ };
67
+ const NAMING_VEIN_PARTS = {
68
+ functional: {
69
+ left: ["Name", "Naming", "Nym", "Mark", "Decision", "Signal", "Evidence", "Proof", "Brand", "Launch"],
70
+ right: ["Scout", "Guide", "Lens", "Ledger", "Dossier", "Map", "Signal", "Check", "Record", "Verdict"],
71
+ rationale: "Makes the naming-decision workflow legible without presenting the product as a generic generator.",
72
+ },
73
+ outcome: {
74
+ left: ["Ready", "Clear", "Chosen", "Settled", "Done", "Sure", "Launch", "Found", "Final", "True"],
75
+ right: ["Name", "Choice", "Mark", "Signal", "Decision", "Launch", "Greenlight", "Result", "Pick", "Path"],
76
+ rationale: "Centers the move from naming uncertainty to a decision the founder can ship.",
77
+ },
78
+ metaphor: {
79
+ left: ["Cairn", "Beacon", "Compass", "Harbor", "Docket", "Verdict", "Record", "Trail", "Signal", "North"],
80
+ right: ["Mark", "Guide", "Light", "Path", "Point", "Ledger", "Line", "Proof", "Arc", "Gate"],
81
+ rationale: "Uses an evidence, navigation, or decision metaphor with room for a broader launch workflow.",
82
+ },
83
+ lateral: {
84
+ left: ["Indie", "Plain", "Quiet", "Open", "True", "Ready", "Good", "Next", "Side", "Final"],
85
+ right: ["Dossier", "Docket", "Signal", "Record", "Cairn", "Mark", "Choice", "Proof", "Brief", "Found"],
86
+ rationale: "Creates useful distance from commodity AI-generator language while retaining a defensible naming story.",
87
+ },
88
+ };
89
+ function hashString(value) {
90
+ let hash = 2166136261;
91
+ for (let index = 0; index < value.length; index += 1) {
92
+ hash ^= value.charCodeAt(index);
93
+ hash = Math.imul(hash, 16777619);
94
+ }
95
+ return hash >>> 0;
96
+ }
97
+ function rotate(values, offset) {
98
+ if (!values.length)
99
+ return [];
100
+ const normalized = Math.abs(offset) % values.length;
101
+ return [...values.slice(normalized), ...values.slice(0, normalized)];
102
+ }
103
+ function titleWord(value) {
104
+ const cleaned = (0, normalize_1.normalizeName)(value).replace(/\s+/g, "");
105
+ return cleaned ? `${cleaned[0].toUpperCase()}${cleaned.slice(1)}` : "";
106
+ }
107
+ function rootsFromBrief(brief) {
108
+ const fields = [brief.product, brief.outcome, brief.mechanism, brief.pain];
109
+ const words = fields
110
+ .join(" ")
111
+ .normalize("NFKD")
112
+ .replace(/\p{M}+/gu, "")
113
+ .toLowerCase()
114
+ .match(/[a-z][a-z0-9-]{2,14}/g);
115
+ // Keep roots as complete source words. A broad suffix stripper previously
116
+ // turned ordinary product language into fragments such as "naming" ->
117
+ // "nam" and "registered" -> "regist", which then leaked into generated
118
+ // names and score explanations.
119
+ const roots = (words ?? [])
120
+ .filter((word) => word.length >= 3 && word.length <= 11 && !STOP_WORDS.has(word));
121
+ return [...new Set(roots)].slice(0, 12);
122
+ }
123
+ function parseOptions(options) {
124
+ const input = typeof options === "number" ? { count: options } : options ?? {};
125
+ return {
126
+ count: Math.max(1, Math.min(200, Math.floor(input.count ?? 36))),
127
+ avoid: input.avoid ?? [],
128
+ existingNames: input.existingNames ?? [],
129
+ veins: input.veins ?? [],
130
+ };
131
+ }
132
+ /**
133
+ * A no-key fallback that creates a diverse, inspectable starting set. It does
134
+ * not imply availability or diligence; live checks happen after generation.
135
+ */
136
+ function generateFallbackCandidates(input, options) {
137
+ const brief = typeof input === "string" ? (0, brief_1.compileBrief)(input) : input;
138
+ const config = parseOptions(options);
139
+ const seed = hashString(JSON.stringify(brief));
140
+ const avoidKeys = new Set([...brief.avoid, ...config.avoid, ...config.existingNames].map((value) => (0, normalize_1.comparisonKey)(value)).filter(Boolean));
141
+ const roots = rootsFromBrief(brief);
142
+ const workoutProfile = /\b(?:workout|exercise|reps?|sets?|cadence|fitness)\b/i.test([brief.product, brief.pain, brief.outcome, brief.mechanism].join(" "));
143
+ const namingProfile = /\b(?:product naming|naming decision|name candidates?|brand name|domain evidence|name generator)\b/i.test([brief.product, brief.pain, brief.outcome, brief.mechanism].join(" "));
144
+ const partsFor = (vein) => (workoutProfile ? WORKOUT_VEIN_PARTS[vein] : undefined) ??
145
+ (namingProfile ? NAMING_VEIN_PARTS[vein] : undefined) ??
146
+ VEIN_PARTS[vein];
147
+ const publicVeins = ["functional", "outcome", "metaphor", "lateral"];
148
+ const configuredVeins = config.veins.filter((vein) => publicVeins.includes(vein));
149
+ const veins = [...new Set(configuredVeins.length ? configuredVeins : publicVeins)];
150
+ const result = [];
151
+ const seen = new Set(avoidKeys);
152
+ const add = (name, vein, rationale) => {
153
+ const normalized = (0, normalize_1.normalizeName)(name);
154
+ const key = (0, normalize_1.comparisonKey)(name);
155
+ if (!key || key.length < 4 || key.length > 24 || seen.has(key))
156
+ return;
157
+ if ((0, brief_constraints_1.candidateAvoidViolations)(name, brief).length)
158
+ return;
159
+ if ([...avoidKeys].some((avoid) => avoid.length >= 3 && key.includes(avoid)))
160
+ return;
161
+ seen.add(key);
162
+ result.push({ name, normalized, vein, rationale, provider: "deterministic-fallback" });
163
+ };
164
+ // Start every direction before filling deeper combinations. This protects
165
+ // diversity when a caller requests a small batch.
166
+ for (let veinIndex = 0; veinIndex < veins.length && result.length < config.count; veinIndex += 1) {
167
+ const vein = veins[veinIndex];
168
+ const parts = partsFor(vein);
169
+ const left = rotate(parts.left, seed + veinIndex * 3);
170
+ const right = rotate(parts.right, (seed >>> 3) + veinIndex * 5);
171
+ add(`${left[0]}${right[0]}`, vein, parts.rationale);
172
+ }
173
+ for (let pass = 0; result.length < config.count && pass < 30; pass += 1) {
174
+ for (let veinIndex = 0; veinIndex < veins.length && result.length < config.count; veinIndex += 1) {
175
+ const vein = veins[veinIndex];
176
+ const parts = partsFor(vein);
177
+ const left = rotate(parts.left, seed + veinIndex * 3);
178
+ const right = rotate(parts.right, (seed >>> 3) + veinIndex * 5);
179
+ // The workout profile already has a curated vocabulary. Raw roots such
180
+ // as "audio-first" or "autopilot" create clumsy compounds and can
181
+ // collapse back into feature labels, so reserve root injection for the
182
+ // generic fallback profile.
183
+ const briefRoot = workoutProfile || namingProfile
184
+ ? ""
185
+ : titleWord(roots[(pass + veinIndex) % Math.max(roots.length, 1)] ?? "");
186
+ if (briefRoot && vein !== "coined" && pass % 2 === 0) {
187
+ const rootFirst = vein === "functional" || vein === "workflow";
188
+ add(rootFirst ? `${briefRoot}${right[pass % right.length]}` : `${left[pass % left.length]}${briefRoot}`, vein, `${parts.rationale} Draws on “${briefRoot}” from the editable project brief.`);
189
+ }
190
+ else {
191
+ add(`${left[pass % left.length]}${right[(pass * 3 + veinIndex) % right.length]}`, vein, parts.rationale);
192
+ }
193
+ }
194
+ }
195
+ // Preserve the brief-led ordering above for ordinary batches, then exhaust
196
+ // deterministic left/right combinations so sparse briefs can still fill a
197
+ // bounded 200-name creator sprint without inventing availability evidence.
198
+ for (let pair = 0; result.length < config.count && pair < 100; pair += 1) {
199
+ for (let veinIndex = 0; veinIndex < veins.length && result.length < config.count; veinIndex += 1) {
200
+ const vein = veins[veinIndex];
201
+ const parts = partsFor(vein);
202
+ const left = rotate(parts.left, seed + veinIndex * 3);
203
+ const right = rotate(parts.right, (seed >>> 3) + veinIndex * 5);
204
+ const leftIndex = Math.floor(pair / right.length) % left.length;
205
+ const rightIndex = (pair + veinIndex * 3) % right.length;
206
+ add(`${left[leftIndex]}${right[rightIndex]}`, vein, parts.rationale);
207
+ }
208
+ }
209
+ return result.slice(0, config.count);
210
+ }