@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,409 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scoreName = scoreName;
4
+ exports.scoreNames = scoreNames;
5
+ const normalize_1 = require("./normalize");
6
+ const brief_constraints_1 = require("./brief-constraints");
7
+ const DEFAULT_SATURATED_ROOTS = ["ai", "bot", "gpt", "gen", "smart", "brand", "name", "app", "hub", "ify"];
8
+ const BLOCKING_SIGNALS = new Set([
9
+ "active_same_category_collision",
10
+ "hostile_domain_redirect",
11
+ "family_of_marks",
12
+ "voice_transcription_failure",
13
+ "negative_meaning",
14
+ "scope_lock",
15
+ ]);
16
+ function clamp(value) {
17
+ return Math.max(0, Math.min(100, Math.round(value)));
18
+ }
19
+ function dimension(key, label, score, weight, note, source, manual = false) {
20
+ return { key, label, score: clamp(score), weight, note, source, manual };
21
+ }
22
+ function wordRoots(value) {
23
+ const words = (0, normalize_1.normalizeName)(value)
24
+ .split(" ")
25
+ .filter((word) => word.length >= 3);
26
+ return [...new Set(words.flatMap((word) => {
27
+ if (word.length > 4 && word.endsWith("ies"))
28
+ return [word, `${word.slice(0, -3)}y`];
29
+ if (word.length > 4 && word.endsWith("s") && !word.endsWith("ss"))
30
+ return [word, word.slice(0, -1)];
31
+ return [word];
32
+ }))];
33
+ }
34
+ function lengthScore(name) {
35
+ const length = (0, normalize_1.comparisonKey)(name).length;
36
+ if (length >= 5 && length <= 10)
37
+ return 94;
38
+ if (length >= 3 && length <= 13)
39
+ return 82;
40
+ if (length <= 16)
41
+ return 66;
42
+ if (length <= 20)
43
+ return 45;
44
+ return 24;
45
+ }
46
+ function pronunciationScore(name) {
47
+ const key = (0, normalize_1.comparisonKey)(name);
48
+ if (!key)
49
+ return { score: 0, note: "No pronounceable letters were found." };
50
+ let score = 92;
51
+ const warnings = [];
52
+ if (/\d/.test(key)) {
53
+ score -= 12;
54
+ warnings.push("digits require a spoken convention");
55
+ }
56
+ if (/[^aeiouy]{4,}/.test(key)) {
57
+ score -= 28;
58
+ warnings.push("long consonant cluster");
59
+ }
60
+ if (/q(?!u)/.test(key)) {
61
+ score -= 16;
62
+ warnings.push("q without u is ambiguous");
63
+ }
64
+ if (/(?:[aeiou]{3,}|(.)\1\1)/.test(key)) {
65
+ score -= 12;
66
+ warnings.push("unusual repeated sound or letter");
67
+ }
68
+ const syllableGroups = key.match(/[aeiouy]+/g)?.length ?? 0;
69
+ if (syllableGroups === 0) {
70
+ score -= 45;
71
+ warnings.push("no obvious vowel sound");
72
+ }
73
+ else if (syllableGroups >= 5) {
74
+ score -= 18;
75
+ warnings.push("likely five or more syllables");
76
+ }
77
+ return {
78
+ score,
79
+ note: warnings.length
80
+ ? `Mechanical warning: ${warnings.join(", ")}. A real dictation test is still required.`
81
+ : "No obvious text-level pronunciation issue; this is not a speech-recognition test.",
82
+ };
83
+ }
84
+ function spellingScore(name) {
85
+ const raw = name.trim();
86
+ const key = (0, normalize_1.comparisonKey)(name);
87
+ let score = 91;
88
+ const issues = [];
89
+ if (/[-_.\s]/.test(raw)) {
90
+ score -= 8;
91
+ issues.push("word boundary may need explanation");
92
+ }
93
+ if (/\d/.test(raw)) {
94
+ score -= 18;
95
+ issues.push("digit-versus-word ambiguity");
96
+ }
97
+ if (/[xzq]{2}|ph|ght|ae|oe/.test(key)) {
98
+ score -= 9;
99
+ issues.push("more than one plausible spelling");
100
+ }
101
+ if (/[^a-z0-9\s-]/i.test(raw)) {
102
+ score -= 20;
103
+ issues.push("punctuation is not portable across namespaces");
104
+ }
105
+ return {
106
+ score,
107
+ note: issues.length ? `${issues.join("; ")}. Verify by asking someone to spell it after hearing it.` : "Text form is compact and mechanically predictable; verify with a listener.",
108
+ };
109
+ }
110
+ function semanticFit(name, brief) {
111
+ if (!brief)
112
+ return { score: 50, note: "No brief was supplied, so strategic fit is unscored." };
113
+ const roots = new Set(wordRoots([brief.product, brief.pain, brief.outcome, brief.mechanism, ...brief.brandTone].join(" ")));
114
+ const nameKey = (0, normalize_1.comparisonKey)(name);
115
+ const hits = [...roots].filter((root) => nameKey.includes(root));
116
+ if (hits.length >= 2)
117
+ return { score: 91, note: `Connects directly to brief concepts: ${hits.slice(0, 3).join(", ")}.` };
118
+ if (hits.length === 1) {
119
+ const cliché = DEFAULT_SATURATED_ROOTS.find((root) => nameKey.startsWith(root) || nameKey.endsWith(root));
120
+ if (cliché)
121
+ return { score: 68, note: `Carries the brief root “${hits[0]},” but relies on the saturated “${cliché}” pattern.` };
122
+ return { score: 80, note: `Carries one explicit brief concept: ${hits[0]}.` };
123
+ }
124
+ return {
125
+ score: 38,
126
+ note: "No literal brief root is present. Treat this as an intentionally lateral direction until a human can explain its connection.",
127
+ };
128
+ }
129
+ function distinctiveness(name, context) {
130
+ const key = (0, normalize_1.comparisonKey)(name);
131
+ const workoutCategory = /\b(?:workout|exercise|fitness|reps?|sets?|cadence|bodyweight)\b/i.test([context.brief?.product, context.brief?.pain, context.brief?.outcome, context.brief?.mechanism, context.category].filter(Boolean).join(" "));
132
+ const contextualRoots = workoutCategory
133
+ ? ["workout", "fitness", "gym", "rep", "set", "tempo", "pace", "cadence", "timer", "counter", "guide"]
134
+ : [];
135
+ const roots = [...DEFAULT_SATURATED_ROOTS, ...(context.saturatedRoots ?? [])]
136
+ .map(normalize_1.comparisonKey)
137
+ .filter((root) => root.length >= 2);
138
+ const matched = roots.filter((root) => key.startsWith(root) || key.endsWith(root));
139
+ const contextualMatched = contextualRoots.filter((root) => key.startsWith(root) || key.endsWith(root) || key.endsWith(`${root}s`));
140
+ const competitors = (context.competitorNames ?? []).map(normalize_1.comparisonKey).filter(Boolean);
141
+ const exact = competitors.find((competitor) => competitor === key);
142
+ const near = competitors.find((competitor) => competitor !== key && (competitor.includes(key) || key.includes(competitor)) && Math.min(competitor.length, key.length) >= 5);
143
+ if (exact)
144
+ return { score: 0, note: "Exact competitor-name collision supplied in the scoring context." };
145
+ if (near)
146
+ return { score: 24, note: `Strong lexical overlap with supplied competitor “${near}”.` };
147
+ const briefRoots = context.brief
148
+ ? [...new Set(wordRoots([context.brief.product, context.brief.pain, context.brief.outcome, context.brief.mechanism].join(" ")))]
149
+ : [];
150
+ const descriptiveRoots = briefRoots.filter((root) => key.includes(root));
151
+ if (descriptiveRoots.length >= 2) {
152
+ return {
153
+ score: 46,
154
+ note: `Combines multiple literal category roots (${descriptiveRoots.slice(0, 3).join(", ")}); clear, but likely descriptive and crowded.`,
155
+ };
156
+ }
157
+ if (descriptiveRoots.length === 1 && key === (0, normalize_1.comparisonKey)(descriptiveRoots[0])) {
158
+ return { score: 52, note: `Uses only the literal category root “${descriptiveRoots[0]},” which is clear but difficult to own.` };
159
+ }
160
+ if (contextualMatched.length >= 2)
161
+ return { score: 44, note: `Combines common fitness naming roots: ${contextualMatched.join(", ")}.` };
162
+ if (contextualMatched.length === 1)
163
+ return { score: 64, note: `Uses the familiar fitness naming root “${contextualMatched[0]}”; compare category crowding carefully.` };
164
+ if (matched.length >= 2)
165
+ return { score: 48, note: `Uses saturated category patterns: ${matched.join(", ")}.` };
166
+ if (matched.length === 1)
167
+ return { score: 48, note: `Uses a saturated category pattern: ${matched[0]}.` };
168
+ return { score: 86, note: "No supplied competitor collision or default saturated affix was found." };
169
+ }
170
+ function domainScore(context) {
171
+ const results = context.domainResults ?? context.candidate?.domains ?? [];
172
+ if (!results.length)
173
+ return { score: 50, note: "No live domain evidence supplied; domain quality remains unknown." };
174
+ const preferred = context.preferredTld?.replace(/^\./, "").toLowerCase();
175
+ const result = results.find((item) => preferred && item.domain.toLowerCase().endsWith(`.${preferred}`)) ??
176
+ results.find((item) => item.status === "likely_available") ??
177
+ results.find((item) => item.status === "premium" || item.status === "parked") ??
178
+ results[0];
179
+ const scores = {
180
+ likely_available: 93,
181
+ registered: 38,
182
+ premium: 62,
183
+ parked: 47,
184
+ active: 22,
185
+ unsupported: 45,
186
+ unknown: 45,
187
+ };
188
+ const qualifiers = {
189
+ likely_available: "Registry absence is an inferred signal, not a purchase guarantee.",
190
+ registered: "The domain is registered; ownership, use, and acquisition posture still matter.",
191
+ premium: "A premium option can be rational, but price and renewal terms need confirmation.",
192
+ parked: "The matching domain appears parked and may be acquirable.",
193
+ active: "An active matching site materially weakens the domain posture.",
194
+ unsupported: "The TLD is unsupported by the live checker.",
195
+ unknown: "Domain status is unknown because the check did not produce a trustworthy answer.",
196
+ };
197
+ return { score: scores[result.status] ?? 45, note: `${result.domain}: ${qualifiers[result.status]}` };
198
+ }
199
+ function namespaceScore(context) {
200
+ const results = context.namespaceResults ?? context.candidate?.namespaces ?? [];
201
+ if (!results.length)
202
+ return { score: 50, note: "No developer or app-store namespace evidence supplied." };
203
+ const conflicts = results.filter((item) => item.status === "taken" || item.status === "conflict");
204
+ const unknown = results.filter((item) => item.status === "unknown" || item.status === "manual");
205
+ if (conflicts.length >= 3)
206
+ return { score: 26, note: `${conflicts.length} exact namespace collisions were supplied.` };
207
+ if (conflicts.length)
208
+ return { score: 55, note: `${conflicts.length} exact namespace collision(s) need review.` };
209
+ if (unknown.length)
210
+ return { score: 67, note: `No confirmed collision, but ${unknown.length} result(s) remain unknown or manual.` };
211
+ return { score: 91, note: "The supplied exact namespace checks found no collision; broader namesakes still need research." };
212
+ }
213
+ function extensibilityScore(name, context) {
214
+ if (context.riskSignals?.some((signal) => signal.kind === "scope_lock" && signal.status !== "unknown")) {
215
+ return { score: 10, note: "A supplied scope-lock signal conflicts with the roadmap." };
216
+ }
217
+ const key = (0, normalize_1.comparisonKey)(name);
218
+ const narrow = ["ios", "iphone", "pouch", "tiktok", "chrome", "twitter", "shopify", "wordpress"].find((root) => key.includes(root));
219
+ if (narrow)
220
+ return { score: 42, note: `“${narrow}” may bind the brand to one platform or vertical.` };
221
+ const movementExpansion = /\b(?:multiple movement|beyond pushups|future movement|exercise groups?|circuits?)\b/i.test(context.brief?.futureExpansion ?? "");
222
+ const repLock = movementExpansion && ["rep", "pushup", "squat", "burpee", "timer", "counter"].find((root) => key.includes(root));
223
+ if (repLock)
224
+ return { score: 64, note: `“${repLock}” may narrow a roadmap that explicitly spans multiple movement types.` };
225
+ return context.brief?.futureExpansion
226
+ ? { score: 82, note: "No obvious platform lock was found; compare the metaphor against the stated future scope." }
227
+ : { score: 62, note: "No obvious platform lock was found, but future scope was not explicit." };
228
+ }
229
+ function channelProfiles(context, candidate) {
230
+ const supplied = [
231
+ ...(context.brief?.acquisitionChannels ?? []),
232
+ ...(context.channels ?? []),
233
+ ]
234
+ .map((item) => item.trim())
235
+ .filter(Boolean);
236
+ const category = (context.category ?? candidate?.category ?? "").trim();
237
+ const values = [...supplied, category].map((item) => item.toLocaleLowerCase("en-US"));
238
+ const profiles = new Set();
239
+ if (values.some((value) => /\b(?:word[ -]?of[ -]?mouth|spoken|voice|creator|ugc|video|tiktok|reels?|shorts?|podcasts?|radio|referrals?)\b/.test(value))) {
240
+ profiles.add("spoken");
241
+ }
242
+ if (values.some((value) => /\b(?:developer|software|agents?|codex|claude|cli|api|mcp|github|npm|pypi)\b/.test(value))) {
243
+ profiles.add("developer");
244
+ }
245
+ if (values.some((value) => /\b(?:app[ -]?stores?|consumer[ -]?app|health[ -]?app|mobile|ios|android|aso)\b/.test(value))) {
246
+ profiles.add("app_store");
247
+ }
248
+ if (values.some((value) => /\b(?:search|seo|publication|content|editorial)\b/.test(value))) {
249
+ profiles.add("search");
250
+ }
251
+ if (values.some((value) => /\b(?:business|sales|enterprise|outbound|partnerships?)\b/.test(value))) {
252
+ profiles.add("sales");
253
+ }
254
+ return {
255
+ profiles: [...profiles],
256
+ supplied: [...new Set([...(category ? [`category: ${category}`] : []), ...supplied])],
257
+ };
258
+ }
259
+ function channelFitScore(name, context, candidate, inputs) {
260
+ const { profiles, supplied } = channelProfiles(context, candidate);
261
+ if (!profiles.length) {
262
+ return {
263
+ score: 50,
264
+ note: "No acquisition channel or recognized product category was supplied, so channel fit remains a conservative unknown.",
265
+ };
266
+ }
267
+ const profileScores = {
268
+ // Text heuristics cannot prove dictation or caption accuracy, so spoken fit
269
+ // stays capped until a person runs the actual speech tests.
270
+ spoken: Math.min(76, inputs.pronunciation * 0.6 + inputs.spelling * 0.4),
271
+ developer: Math.min(84, inputs.spelling * 0.45 + inputs.length * 0.25 + inputs.namespace * 0.3),
272
+ app_store: Math.min(82, inputs.length * 0.35 + inputs.pronunciation * 0.3 + inputs.namespace * 0.35),
273
+ search: Math.min(76, inputs.spelling * 0.45 + inputs.semantic * 0.35 + inputs.distinctiveness * 0.2),
274
+ sales: Math.min(78, inputs.pronunciation * 0.45 + inputs.spelling * 0.3 + inputs.semantic * 0.25),
275
+ };
276
+ const score = profiles.reduce((sum, profile) => sum + profileScores[profile], 0) / profiles.length;
277
+ const focus = profiles
278
+ .map((profile) => ({
279
+ spoken: "spoken recall and dictation",
280
+ developer: "typing and developer namespaces",
281
+ app_store: "title fit and app-store collisions",
282
+ search: "spelling and search legibility",
283
+ sales: "sayability and explanation",
284
+ })[profile])
285
+ .join(", ");
286
+ return {
287
+ score,
288
+ note: `Context (${supplied.join(", ")}) emphasizes ${focus}. This is a mechanical proxy for “${name},” not a real audience, dictation, caption, or conversion test.`,
289
+ };
290
+ }
291
+ function classifySignals(signals = []) {
292
+ const blockers = [];
293
+ const warnings = [];
294
+ for (const signal of signals) {
295
+ const confirmed = signal.status === "confirmed" || signal.status === "failed";
296
+ if ((signal.severity === "blocker" || BLOCKING_SIGNALS.has(signal.kind)) && confirmed)
297
+ blockers.push(signal.detail);
298
+ else if (signal.kind === "trademark_unfinished")
299
+ warnings.push(`Preliminary trademark work is unfinished: ${signal.detail}`);
300
+ else if (signal.status === "unknown" || signal.status === "manual")
301
+ warnings.push(`Unresolved: ${signal.detail}`);
302
+ else if (signal.severity === "high" || signal.severity === "medium")
303
+ warnings.push(signal.detail);
304
+ }
305
+ return { blockers: [...new Set(blockers)], warnings: [...new Set(warnings)] };
306
+ }
307
+ function asContext(input) {
308
+ if (!input)
309
+ return {};
310
+ if ("product" in input && "primaryUser" in input && !("brief" in input))
311
+ return { brief: input };
312
+ return input;
313
+ }
314
+ /** A transparent triage score. It never substitutes for collision, speech, or legal diligence. */
315
+ function scoreName(nameOrCandidate, inputContext) {
316
+ const candidate = typeof nameOrCandidate === "string" ? undefined : nameOrCandidate;
317
+ const name = typeof nameOrCandidate === "string" ? nameOrCandidate : nameOrCandidate.name;
318
+ if (!(0, normalize_1.normalizeName)(name))
319
+ throw new TypeError("A non-empty candidate name is required.");
320
+ const baseContext = asContext(inputContext);
321
+ const context = {
322
+ ...baseContext,
323
+ domainResults: baseContext.domainResults ?? candidate?.domains,
324
+ namespaceResults: baseContext.namespaceResults ?? candidate?.namespaces,
325
+ };
326
+ const pronunciation = pronunciationScore(name);
327
+ const spelling = spellingScore(name);
328
+ const fit = semanticFit(name, context.brief);
329
+ const difference = distinctiveness(name, context);
330
+ const domain = domainScore(context);
331
+ const namespace = namespaceScore(context);
332
+ const extensibility = extensibilityScore(name, context);
333
+ const founderProvided = Number.isFinite(context.founderResonance);
334
+ const founderScore = founderProvided ? clamp(context.founderResonance) : 50;
335
+ const channel = channelFitScore(name, context, candidate, {
336
+ pronunciation: pronunciation.score,
337
+ spelling: spelling.score,
338
+ length: lengthScore(name),
339
+ semantic: fit.score,
340
+ distinctiveness: difference.score,
341
+ namespace: namespace.score,
342
+ });
343
+ const evidenceWeight = founderProvided ? 0.88 : 1;
344
+ const weight = (base) => base * evidenceWeight;
345
+ const dimensions = [
346
+ dimension("strategic_fit", "Strategic fit", fit.score, weight(0.22), fit.note, "brief"),
347
+ dimension("memorability", "Memorability", (lengthScore(name) + difference.score) / 2, weight(0.09), "A compact, differentiated text form is easier to retain; real recall testing is still human.", "mechanical"),
348
+ dimension("distinctiveness", "Distinctiveness", difference.score, weight(0.14), difference.note, "brief"),
349
+ dimension("pronunciation", "Pronunciation", pronunciation.score, weight(0.09), pronunciation.note, "mechanical"),
350
+ dimension("spelling", "Spelling predictability", spelling.score, weight(0.07), spelling.note, "mechanical"),
351
+ dimension("length", "Length and title fit", lengthScore(name), weight(0.04), `${(0, normalize_1.comparisonKey)(name).length} alphanumeric characters; channel-specific title limits still need checking.`, "mechanical"),
352
+ dimension("domain", "Domain posture", domain.score, weight(0.11), domain.note, "evidence"),
353
+ dimension("namespace", "Namespace posture", namespace.score, weight(0.07), namespace.note, "evidence"),
354
+ dimension("extensibility", "Future extensibility", extensibility.score, weight(0.08), extensibility.note, "brief"),
355
+ dimension("channel_fit", "Channel and word-of-mouth fit", channel.score, weight(0.09), channel.note, context.brief || context.channels?.length || context.category || candidate?.category
356
+ ? "brief"
357
+ : "mechanical"),
358
+ dimension("founder_resonance", "Founder resonance", founderScore, founderProvided ? 0.12 : 0, founderProvided
359
+ ? "Human-entered resonance is included. It may legitimately outweigh a small score delta."
360
+ : "Human judgment required. This dimension is excluded from the total until the founder supplies it.", "human", !founderProvided),
361
+ ];
362
+ const weighted = dimensions.filter((item) => item.weight > 0);
363
+ const totalWeight = weighted.reduce((sum, item) => sum + item.weight, 0);
364
+ let overall = weighted.reduce((sum, item) => sum + item.score * item.weight, 0) / totalWeight;
365
+ const risks = classifySignals(context.riskSignals);
366
+ const briefConflicts = (0, brief_constraints_1.candidateBriefConflicts)(name, context.brief);
367
+ const blockers = [...new Set([...risks.blockers, ...briefConflicts.blockers])];
368
+ const warnings = [...risks.warnings, ...briefConflicts.warnings];
369
+ overall -= briefConflicts.penalty;
370
+ if (!context.domainResults?.length)
371
+ warnings.push("Domain status is unknown until a live registry check runs.");
372
+ if (!context.namespaceResults?.length)
373
+ warnings.push("Developer and app-store namespaces have not been checked.");
374
+ if (!context.riskSignals?.some((signal) => signal.kind.includes("trademark"))) {
375
+ warnings.push("Preliminary trademark similarity research has not been supplied; this score is not legal clearance.");
376
+ }
377
+ if (!founderProvided)
378
+ warnings.push("Founder resonance remains a human decision and is not included in the total.");
379
+ if (blockers.length)
380
+ overall = Math.min(overall, 39);
381
+ const rounded = clamp(overall);
382
+ const label = blockers.length
383
+ ? "Blocked pending resolution"
384
+ : rounded >= 84
385
+ ? "Strong signal"
386
+ : rounded >= 70
387
+ ? "Worth investigating"
388
+ : rounded >= 55
389
+ ? "Mixed signal"
390
+ : "Needs work";
391
+ const summary = blockers.length
392
+ ? `${name} has ${blockers.length} hard blocker${blockers.length === 1 ? "" : "s"}; the numeric score must not override them.`
393
+ : `${name} is a preliminary ${label.toLowerCase()}; live collision evidence and founder judgment remain separate.`;
394
+ return {
395
+ overall: rounded,
396
+ label,
397
+ dimensions,
398
+ hardBlockers: blockers,
399
+ warnings: [...new Set(warnings)],
400
+ summary,
401
+ founderResonanceRequired: !founderProvided,
402
+ };
403
+ }
404
+ function scoreNames(names, context) {
405
+ return names.map((name, index) => {
406
+ const resolved = typeof context === "function" ? context(typeof name === "string" ? name : name.name, index) : context;
407
+ return scoreName(name, resolved);
408
+ });
409
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ /** Shared, framework-independent contracts used by the web, API, CLI, and agents. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizePublicHttpsUrlInput = normalizePublicHttpsUrlInput;
4
+ const EXPLICIT_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
5
+ const HTTP_SCHEME = /^http:\/\//i;
6
+ const HTTPS_SCHEME = /^https:\/\//i;
7
+ const SCHEME_RELATIVE = /^\/\//;
8
+ const HOST_WITH_PORT = /^[^/?#\s]+:\d+(?:[/?#]|$)/;
9
+ /**
10
+ * Turn founder-friendly public web input into an HTTPS URL candidate.
11
+ * Safety validation still happens after this normalization.
12
+ */
13
+ function normalizePublicHttpsUrlInput(input) {
14
+ const raw = input instanceof URL ? input.toString().trim() : input.trim();
15
+ if (!raw)
16
+ return "";
17
+ if (HTTPS_SCHEME.test(raw))
18
+ return raw;
19
+ if (HTTP_SCHEME.test(raw))
20
+ return raw.replace(HTTP_SCHEME, "https://");
21
+ if (SCHEME_RELATIVE.test(raw))
22
+ return `https:${raw}`;
23
+ if (HOST_WITH_PORT.test(raw))
24
+ return `https://${raw}`;
25
+ if (EXPLICIT_SCHEME.test(raw))
26
+ return raw;
27
+ return `https://${raw}`;
28
+ }