@astryxdesign/cli 0.7.2 → 0.7.3-canary.06872b4
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/README.md +99 -66
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +115 -3
- package/api/build/blocks/blocks.d.mts +42 -0
- package/api/build/blocks/blocks.mjs +169 -0
- package/api/build/blocks/blocks.test.mjs +108 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/kit/answer.d.mts +14 -0
- package/api/build/kit/answer.mjs +85 -0
- package/api/build/kit/answer.test.mjs +38 -0
- package/api/build/kit/kind.d.mts +25 -0
- package/api/build/kit/kind.mjs +170 -0
- package/api/build/kit/kit.d.mts +2 -4
- package/api/build/kit/kit.mjs +8 -398
- package/api/build/kit/rank.d.mts +3 -32
- package/api/build/kit/rank.mjs +25 -342
- package/api/build/kit/rank.test.mjs +2 -1
- package/api/build/kit/signals.d.mts +60 -0
- package/api/build/kit/signals.mjs +284 -0
- package/api/build/kit/start.d.mts +32 -0
- package/api/build/kit/start.mjs +163 -0
- package/api/build/kit/words.d.mts +72 -0
- package/api/build/kit/words.mjs +202 -0
- package/api/build/tree.d.mts +20 -0
- package/api/build/tree.mjs +52 -0
- package/api/build/tree.test.mjs +177 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/component.mjs +16 -2
- package/api/component/component.test.mjs +15 -0
- package/api/discover/_adapter.mjs +36 -9
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/docs/_adapter.mjs +17 -5
- package/api/docs/docs.test.mjs +7 -1
- package/api/doctor/doctor.mjs +86 -12
- package/api/doctor/doctor.test.mjs +28 -3
- package/api/init/init.doc.mjs +20 -6
- package/api/init/init.test.mjs +18 -2
- package/api/init/init.type.d.mts +3 -3
- package/api/init/init.type.mjs +3 -3
- package/api/init/remove/remove.mjs +17 -4
- package/api/init/remove/remove.test.mjs +114 -2
- package/api/init/run/run.mjs +69 -6
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/pack-check.mjs +123 -4
- package/api/integration/pack-check.test.mjs +87 -0
- package/api/integration/validate-integration-case-disk.test.mjs +111 -0
- package/api/integration/validate-integration-gaps.test.mjs +1964 -0
- package/api/integration/validate-integration.d.mts +11 -2
- package/api/integration/validate-integration.mjs +226 -32
- package/api/integration/validate-integration.type.d.mts +58 -1
- package/api/integration/validate-integration.type.mjs +45 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +66 -2
- package/api/search/_adapter.mjs +118 -10
- package/api/search/_candidate-score.d.mts +40 -0
- package/api/search/_candidate-score.mjs +238 -0
- package/api/search/_cards.json +6098 -0
- package/api/search/_compound.d.mts +50 -0
- package/api/search/_compound.mjs +170 -0
- package/api/search/_doc-searcher.d.mts +4 -0
- package/api/search/_doc-searcher.mjs +23 -0
- package/api/search/_doc-searcher.test.mjs +36 -0
- package/api/search/_doc-searcher.weights.json +1 -0
- package/api/search/_doc-text.d.mts +30 -0
- package/api/search/_doc-text.mjs +87 -0
- package/api/search/_entries.d.mts +52 -0
- package/api/search/_entries.mjs +158 -0
- package/api/search/_features.d.mts +35 -0
- package/api/search/_features.mjs +155 -0
- package/api/search/_gather-components.d.mts +26 -0
- package/api/search/_gather-components.mjs +284 -0
- package/api/search/_gather-docs.d.mts +20 -0
- package/api/search/_gather-docs.mjs +254 -0
- package/api/search/_gather-hooks.d.mts +12 -0
- package/api/search/_gather-hooks.mjs +52 -0
- package/api/search/_gather-templates.d.mts +25 -0
- package/api/search/_gather-templates.mjs +107 -0
- package/api/search/_lexical.d.mts +135 -0
- package/api/search/_lexical.mjs +351 -0
- package/api/search/_main-searcher.d.mts +19 -0
- package/api/search/_main-searcher.mjs +115 -0
- package/api/search/_main-searcher.test.mjs +106 -0
- package/api/search/_main-searcher.weights.json +1 -0
- package/api/search/_module-doc.d.mts +32 -0
- package/api/search/_module-doc.mjs +78 -0
- package/api/search/_order.d.mts +32 -0
- package/api/search/_order.mjs +162 -0
- package/api/search/_order.test.mjs +72 -0
- package/api/search/_priority.d.mts +50 -0
- package/api/search/_priority.mjs +139 -0
- package/api/search/_query-words.d.mts +25 -0
- package/api/search/_query-words.mjs +92 -0
- package/api/search/_query.d.mts +65 -0
- package/api/search/_query.mjs +176 -0
- package/api/search/_result.d.mts +97 -0
- package/api/search/_result.mjs +150 -0
- package/api/search/_score.d.mts +60 -0
- package/api/search/_score.mjs +283 -0
- package/api/search/_synonyms.d.mts +21 -0
- package/api/search/_synonyms.mjs +67 -0
- package/api/search/_template-searcher.d.mts +4 -0
- package/api/search/_template-searcher.mjs +23 -0
- package/api/search/_template-searcher.test.mjs +40 -0
- package/api/search/_template-searcher.weights.json +1 -0
- package/api/search/_titles.d.mts +33 -0
- package/api/search/_titles.mjs +65 -0
- package/api/search/_words.d.mts +64 -0
- package/api/search/_words.mjs +157 -0
- package/api/search/search.d.mts +22 -305
- package/api/search/search.doc.mjs +7 -5
- package/api/search/search.mjs +53 -2483
- package/api/search/search.type.d.mts +6 -1
- package/api/search/search.type.mjs +6 -1
- package/api/search/tree.d.mts +15 -0
- package/api/search/tree.mjs +40 -0
- package/api/search/tree.test.mjs +66 -0
- package/api/search/tree.weights.json +1 -0
- package/api/theme/build/build.packed-old-core.test.mjs +1 -0
- package/api/upgrade/_adapter.d.mts +57 -9
- package/api/upgrade/_adapter.mjs +198 -28
- package/api/upgrade/registry/registry.d.mts +10 -4
- package/api/upgrade/registry/registry.mjs +33 -16
- package/api/upgrade/registry/registry.test.mjs +85 -0
- package/api/upgrade/run/run.d.mts +4 -2
- package/api/upgrade/run/run.mjs +13 -2
- package/api/upgrade/run/run.test.mjs +417 -3
- package/api/upgrade/upgrade.doc.mjs +10 -5
- package/api/upgrade/upgrade.mjs +13 -2
- package/api/upgrade/upgrade.test.mjs +194 -0
- package/api/upgrade/upgrade.type.d.mts +5 -3
- package/api/upgrade/upgrade.type.mjs +8 -6
- package/assets/codemods/transforms/v0.7.0/__tests__/validate-data-token-ownership.test.mjs +11 -0
- package/assets/codemods/transforms/v0.7.0/validate-data-token-ownership.mjs +3 -3
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/template-grading-rubric.doc.mjs +16 -37
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +6 -2
- package/assets/templates/themes/butter/butterTheme.ts +4 -2
- package/assets/templates/themes/stone/stoneTheme.ts +4 -2
- package/assets/templates/themes/y2k/y2kTheme.ts +4 -2
- package/authoring/debug/parse.d.mts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/commands/doctor.mjs +30 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/init.behavior.test.mjs +230 -1
- package/clients/cli/commands/init.doc.mjs +28 -9
- package/clients/cli/commands/init.flag-help.test.mjs +1 -0
- package/clients/cli/commands/init.mjs +2 -2
- package/clients/cli/commands/outside-project.test.mjs +2 -1
- package/clients/cli/commands/setup-nudge.test.mjs +61 -0
- package/clients/cli/commands/upgrade.doc.mjs +4 -2
- package/clients/cli/commands/upgrade.mjs +9 -2
- package/clients/cli/commands/upgrade.subfolder-path.test.mjs +97 -0
- package/clients/cli/index.mjs +19 -1
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +20 -7
- package/foundation/agent-docs/agent-doc-state.mjs +35 -8
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1010 -99
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +678 -35
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/component-discovery.d.mts +37 -0
- package/foundation/discovery/component-discovery.mjs +80 -5
- package/foundation/discovery/component-loader.d.mts +1 -1
- package/foundation/discovery/docs-discovery.mjs +14 -1
- package/foundation/discovery/module-exports.d.mts +41 -0
- package/foundation/discovery/module-exports.mjs +253 -0
- package/foundation/discovery/template-adapter.mjs +18 -1
- package/foundation/discovery/template-needs.mjs +1 -44
- package/foundation/discovery/theme-discovery.mjs +147 -3
- package/foundation/doc-compiler/doc-loads.test.mjs +8 -0
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/fs/paths.d.mts +70 -8
- package/foundation/fs/paths.mjs +556 -38
- package/foundation/fs/paths.test.mjs +430 -54
- package/foundation/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- package/foundation/integrations/computer-integrations.d.mts +6 -3
- package/foundation/integrations/computer-integrations.mjs +38 -9
- package/foundation/integrations/contribution-inventory.d.mts +15 -20
- package/foundation/integrations/contribution-inventory.mjs +41 -5
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/run-context.mjs +14 -0
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/route/route.d.mts +285 -0
- package/foundation/route/route.mjs +324 -0
- package/foundation/route/route.test.mjs +153 -0
- package/foundation/runtime/hosted-runtime.test.mjs +403 -0
- package/foundation/runtime/locked-versions.d.mts +32 -0
- package/foundation/runtime/locked-versions.mjs +65 -0
- package/foundation/runtime/package-root.d.mts +91 -0
- package/foundation/runtime/package-root.mjs +174 -0
- package/foundation/runtime/package-set.d.mts +62 -0
- package/foundation/runtime/package-set.mjs +130 -0
- package/foundation/runtime/package-set.test.mjs +432 -0
- package/host/door-conformance.test.mjs +274 -0
- package/host/index.d.ts +116 -0
- package/host/index.mjs +295 -0
- package/host/index.test.mjs +302 -0
- package/package.json +14 -9
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file A whole query's score against one candidate: the reserved top tier
|
|
5
|
+
* for multi-word queries, then the per-token scores (see _candidate-score.mjs).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {SYNONYM_INDEX} from './_synonyms.mjs';
|
|
9
|
+
import {joined} from './_words.mjs';
|
|
10
|
+
import {phraseWords} from './_query.mjs';
|
|
11
|
+
import {compoundHit, compoundOf} from './_compound.mjs';
|
|
12
|
+
import {scoreCandidate} from './_candidate-score.mjs';
|
|
13
|
+
import {
|
|
14
|
+
TITLE_PHRASE_SCORE,
|
|
15
|
+
TITLE_IN_QUERY_SCORE,
|
|
16
|
+
headingWithPhrase,
|
|
17
|
+
titleInQuery,
|
|
18
|
+
} from './_titles.mjs';
|
|
19
|
+
|
|
20
|
+
/** @typedef {import('./_lexical.mjs').Candidate} Candidate */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Minimum per-token score (in the multi-word pass) to count as a real match.
|
|
24
|
+
* 50 = a genuine name/keyword/description hit; below that is loose Levenshtein
|
|
25
|
+
* fuzz that would otherwise turn gibberish queries into noise.
|
|
26
|
+
*
|
|
27
|
+
* Guidance (45) is deliberately BELOW this floor, so it never counts as one of
|
|
28
|
+
* the matched concepts in a multi-word query. Measured: letting it count moved
|
|
29
|
+
* `nested menu` from SideNav to List and `explain why a field is required` from
|
|
30
|
+
* Field to TextInput — in both cases a component whose guidance happens to
|
|
31
|
+
* mention the other word displaced the one that IS the answer. Breadth is not
|
|
32
|
+
* relevance, the same reason `weakKeywords` are capped. Guidance still decides
|
|
33
|
+
* single-word queries and still breaks ties, which is where it earns its place.
|
|
34
|
+
*/
|
|
35
|
+
const MIN_TOKEN_SCORE = 50;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Best score for a token against a candidate, fanning out through synonyms
|
|
39
|
+
* (synonym hits are discounted so a direct hit always wins).
|
|
40
|
+
* @param {string} tok
|
|
41
|
+
* @param {Candidate} candidate
|
|
42
|
+
* @param {{fuzzy?: boolean}} [opts]
|
|
43
|
+
* @returns {{score: number, reason: string} | null}
|
|
44
|
+
*/
|
|
45
|
+
export function bestForToken(tok, candidate, opts = {}) {
|
|
46
|
+
let best = scoreCandidate(tok, candidate, opts);
|
|
47
|
+
const syns = SYNONYM_INDEX.get(tok);
|
|
48
|
+
if (syns) {
|
|
49
|
+
for (const s of syns) {
|
|
50
|
+
const h = scoreCandidate(s, candidate, opts);
|
|
51
|
+
if (h) {
|
|
52
|
+
const score = Math.round(h.score * 0.85);
|
|
53
|
+
if (!best || score > best.score)
|
|
54
|
+
best = {score, reason: `${h.reason} (~${tok})`};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return best;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The score of a candidate that matches every content word of a multi-word
|
|
63
|
+
* query, before a bonus of up to 8 for how strong its strongest match is: just
|
|
64
|
+
* above anything that matches only some of the words. The token-sum path tops
|
|
65
|
+
* out near 150 for a partial match (a 100 on one word, the per-word bonus, and
|
|
66
|
+
* the coverage term), so an AND-match with one keyword-strength hit (see
|
|
67
|
+
* {@link STRONG_TOKEN_SCORE}) always outranks an OR-match, and stays below the
|
|
68
|
+
* title tiers.
|
|
69
|
+
*/
|
|
70
|
+
export const FULL_COVERAGE_SCORE = 151;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The strongest single-word hit an every-word match needs to take that tier: a
|
|
74
|
+
* keyword substring. Two passing mentions in prose, or the components a page
|
|
75
|
+
* happens to render, are breadth, not relevance; they stay on the token sum,
|
|
76
|
+
* below an exact name or keyword hit on one of the words.
|
|
77
|
+
*/
|
|
78
|
+
export const STRONG_TOKEN_SCORE = 70;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} term - Lowercased full query.
|
|
82
|
+
* @param {string[]} tokens - Content tokens from tokenizeQuery(term).
|
|
83
|
+
* @param {Candidate} candidate
|
|
84
|
+
* @param {string[]} [words] - The query's words for compound names (see
|
|
85
|
+
* compoundHit); empty when the query names an entry outright.
|
|
86
|
+
* @param {{plain?: boolean}} [opts] - `plain`: score without matches on part
|
|
87
|
+
* of a name or a joined keyword.
|
|
88
|
+
* @returns {{score: number, reason: string, matched: number, total: number, alias?: 'partial' | 'keyword', aliasWords?: number} | null}
|
|
89
|
+
* `matched`/`total` are the query concepts this candidate answered, out of
|
|
90
|
+
* the concepts the query had. Callers that must distinguish "matched one word
|
|
91
|
+
* of three" from "matched all three" — `build`, which gates its pages group
|
|
92
|
+
* on coverage — cannot recover that from the score, because a single strong
|
|
93
|
+
* hit and a broad weak one land on the same number.
|
|
94
|
+
*/
|
|
95
|
+
export function scoreQuery(
|
|
96
|
+
term,
|
|
97
|
+
tokens,
|
|
98
|
+
candidate,
|
|
99
|
+
words = phraseWords(term),
|
|
100
|
+
{plain = false} = {},
|
|
101
|
+
) {
|
|
102
|
+
const total = Math.max(tokens.length, 1);
|
|
103
|
+
// A whole-phrase hit answered the whole query by definition.
|
|
104
|
+
const asFull = (/** @type {{score: number, reason: string}} */ hit) => ({
|
|
105
|
+
...hit,
|
|
106
|
+
matched: total,
|
|
107
|
+
total,
|
|
108
|
+
});
|
|
109
|
+
// Typo tolerance is for one-word lookups. In a multi-word query a near miss
|
|
110
|
+
// is usually a different word, not a typo. One typed identifier is one
|
|
111
|
+
// word, whatever words it spells.
|
|
112
|
+
const fuzzy = tokens.length <= 1 || !/\s/.test(term);
|
|
113
|
+
const full = scoreCandidate(term, candidate, {
|
|
114
|
+
fuzzy,
|
|
115
|
+
joins: !plain && words.length > 0,
|
|
116
|
+
});
|
|
117
|
+
// A query of several words keeps its phrase tiers below even when stopwords
|
|
118
|
+
// leave one content word: "make an integration" is still the phrase an
|
|
119
|
+
// author declares as a keyword, and "build an integration" still names a
|
|
120
|
+
// title outright, though each tokenizes to `integration` alone.
|
|
121
|
+
const phrase = phraseWords(term).length >= 2 || words.length >= 2;
|
|
122
|
+
let compound = compoundHit(words, candidate);
|
|
123
|
+
if (plain) {
|
|
124
|
+
if (compound?.alias) compound = null;
|
|
125
|
+
} else {
|
|
126
|
+
compoundOf.set(candidate, compound);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 0–1 content tokens: whole-phrase fuzzy matching (typo tolerance for
|
|
130
|
+
* single words), but if stopwords left exactly one DIFFERENT token (e.g.
|
|
131
|
+
* "pricing page" → "pricing"), score that token too and take the stronger. */
|
|
132
|
+
const fewTokens = () => {
|
|
133
|
+
const single =
|
|
134
|
+
tokens.length === 1 ? bestForToken(tokens[0], candidate, {fuzzy}) : null;
|
|
135
|
+
if (full && (!single || full.score >= single.score)) return asFull(full);
|
|
136
|
+
return single ? asFull(single) : null;
|
|
137
|
+
};
|
|
138
|
+
if (tokens.length <= 1 && !phrase) return fewTokens();
|
|
139
|
+
|
|
140
|
+
// The full (untokenized) query matching a candidate's name or a declared
|
|
141
|
+
// keyword VERBATIM — full.score 90 or 100, the only two scoreCandidate
|
|
142
|
+
// outcomes at or above that mark — is a deliberate, explicit label the
|
|
143
|
+
// author chose for exactly this multi-word concept. Promote it to a
|
|
144
|
+
// reserved top tier, safely above the token-sum path's ceiling below
|
|
145
|
+
// (~151: 100 avg + 36 bonus + 15 coverage), so it always outranks a
|
|
146
|
+
// candidate that merely happens to contain several of the query's
|
|
147
|
+
// individual words. Without this, "table of contents" never surfaces
|
|
148
|
+
// Outline (which declares that exact phrase as a keyword) because dozens
|
|
149
|
+
// of Table-related templates each match "table" and "contents" separately
|
|
150
|
+
// and accumulate a higher raw score (#5239).
|
|
151
|
+
if (full && full.score >= 90) {
|
|
152
|
+
return asFull({
|
|
153
|
+
score: full.score + 100,
|
|
154
|
+
reason: full.reason,
|
|
155
|
+
...(full.alias ? {alias: full.alias, aliasWords: words.length} : {}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The whole query standing as a phrase in a doc's title or one of its
|
|
160
|
+
// headings is the next tier down, and still above the token-sum path. The
|
|
161
|
+
// reader named what the section is about, in order: `dark mode` is the
|
|
162
|
+
// "Light/Dark Mode" section. Without this, the title scores a keyword
|
|
163
|
+
// substring (70) and loses to a doc that happens to name `mode` exactly in
|
|
164
|
+
// a code tick (90 on one token, 98 with coverage), so API enum docs outrank
|
|
165
|
+
// the guide section.
|
|
166
|
+
const heading = headingWithPhrase(term, candidate.titles);
|
|
167
|
+
if (heading != null) {
|
|
168
|
+
return asFull({
|
|
169
|
+
score: TITLE_PHRASE_SCORE,
|
|
170
|
+
reason: `title "${heading}" holds the whole query`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (tokens.length <= 1) return fewTokens();
|
|
174
|
+
|
|
175
|
+
// Multi-word natural language: score each content token, counting only
|
|
176
|
+
// strong hits, then reward coverage so candidates matching more terms win.
|
|
177
|
+
let strongest = 0;
|
|
178
|
+
let matched = 0;
|
|
179
|
+
let tokenSum = 0;
|
|
180
|
+
/** @type {string[]} */
|
|
181
|
+
const hitTerms = [];
|
|
182
|
+
// Adjacent words that join into a name or keyword answer each word they
|
|
183
|
+
// join, as strongly as the joined word matches (see compoundHit).
|
|
184
|
+
// Whether a counted word matched only through part of a name or a keyword
|
|
185
|
+
// read joined, not through the entry's own name or a word of it.
|
|
186
|
+
/** @type {'partial' | 'keyword' | false} */
|
|
187
|
+
let alias = false;
|
|
188
|
+
let aliasWords = 0;
|
|
189
|
+
for (const tok of tokens) {
|
|
190
|
+
let h = bestForToken(tok, candidate, {fuzzy});
|
|
191
|
+
if (
|
|
192
|
+
compound?.covered.has(joined(tok)) &&
|
|
193
|
+
compound.score > (h?.score ?? 0)
|
|
194
|
+
) {
|
|
195
|
+
h = compound;
|
|
196
|
+
if (compound.alias && compound.score >= MIN_TOKEN_SCORE) {
|
|
197
|
+
alias = alias === 'partial' ? alias : compound.alias;
|
|
198
|
+
aliasWords = compound.aliasWords ?? 0;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (h && h.score >= MIN_TOKEN_SCORE) {
|
|
202
|
+
if (h.score > strongest) strongest = h.score;
|
|
203
|
+
matched++;
|
|
204
|
+
hitTerms.push(tok);
|
|
205
|
+
tokenSum += h.score;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// The reverse of the title tier, a step lower: the query holds a whole title
|
|
209
|
+
// of two words or more, so the reader asked a question around the section's
|
|
210
|
+
// name ("how do I add dark mode"). Coverage breaks ties between sections
|
|
211
|
+
// that share a title such as "Best Practices".
|
|
212
|
+
const named = titleInQuery(term, candidate.titles);
|
|
213
|
+
if (named != null) {
|
|
214
|
+
return {
|
|
215
|
+
score: TITLE_IN_QUERY_SCORE + Math.min(matched, 9),
|
|
216
|
+
reason: `the query names the title "${named}"`,
|
|
217
|
+
matched,
|
|
218
|
+
total,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (matched === 0) return full ? asFull(full) : null;
|
|
222
|
+
|
|
223
|
+
const reason = `matches ${matched}/${tokens.length} terms: ${hitTerms.join(', ')}`;
|
|
224
|
+
|
|
225
|
+
// Every word matched is its own tier. Summed per word, a doc that matches
|
|
226
|
+
// both words of `troubleshoot integration` in its title and description
|
|
227
|
+
// (50 + bonus + coverage = 77) lost to thirty docs that each match
|
|
228
|
+
// `integration` alone, by name or in a code tick (98-108). The reader asked for
|
|
229
|
+
// both; a candidate that has both comes first, ordered among its peers by
|
|
230
|
+
// how strong its matches are. It needs one keyword-strength hit:
|
|
231
|
+
// every word mentioned in prose, or rendered by a page, is breadth, and
|
|
232
|
+
// stays on the token sum below an exact hit on one word.
|
|
233
|
+
//
|
|
234
|
+
// The TOTAL quality of matches orders candidates within this tier, not
|
|
235
|
+
// just the strongest single hit. A doc matching both words by keyword
|
|
236
|
+
// (90 + 90 = 180) outranks one matching keyword + prose (90 + 50 = 140).
|
|
237
|
+
// Before this, every all-word match whose strongest hit was a keyword (90)
|
|
238
|
+
// scored 157, burying the better match among dozens of ties.
|
|
239
|
+
if (matched === tokens.length && strongest >= STRONG_TOKEN_SCORE) {
|
|
240
|
+
return {
|
|
241
|
+
score:
|
|
242
|
+
FULL_COVERAGE_SCORE +
|
|
243
|
+
Math.min(
|
|
244
|
+
Math.floor((tokenSum - matched * MIN_TOKEN_SCORE) / (matched * 5)),
|
|
245
|
+
8,
|
|
246
|
+
),
|
|
247
|
+
reason,
|
|
248
|
+
matched,
|
|
249
|
+
total,
|
|
250
|
+
...(alias ? {alias, aliasWords} : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Base the score on the STRONGEST concept that matched, plus a bonus per
|
|
255
|
+
// additional matched concept and a coverage term.
|
|
256
|
+
//
|
|
257
|
+
// Deliberately not the mean, and deliberately not divided by total query
|
|
258
|
+
// length. Dividing by total length penalizes verbose / low-fidelity prompts.
|
|
259
|
+
// Dividing by the number of MATCHED tokens (what this used to do) made the
|
|
260
|
+
// score non-monotonic: a second, weaker hit could drag the mean down by more
|
|
261
|
+
// than the coverage bonus added it back, so matching fewer terms well beat
|
|
262
|
+
// matching more terms partially. Concretely, `build "file browser"` scored
|
|
263
|
+
// two form wizards matching only "file" at 98, above the actual file browser
|
|
264
|
+
// matching both terms at 97 — and 98 clears the PAGE_DIRECT gate, so the
|
|
265
|
+
// wrong template was returned as a confident match.
|
|
266
|
+
//
|
|
267
|
+
// Taking the max keeps both properties: a verbose prompt is still scored on
|
|
268
|
+
// the concepts it did hit, and matching a superset of another candidate's
|
|
269
|
+
// terms can never score lower, since every term of the expression is
|
|
270
|
+
// non-decreasing in the set of matched tokens.
|
|
271
|
+
const coverage = matched / tokens.length;
|
|
272
|
+
const tokenScore = Math.round(
|
|
273
|
+
strongest + Math.min(matched - 1, 3) * 12 + coverage * 15,
|
|
274
|
+
);
|
|
275
|
+
if (full && full.score >= tokenScore) return asFull(full);
|
|
276
|
+
return {
|
|
277
|
+
score: tokenScore,
|
|
278
|
+
reason,
|
|
279
|
+
matched,
|
|
280
|
+
total,
|
|
281
|
+
...(alias ? {alias, aliasWords} : {}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
export namespace SYNONYMS {
|
|
5
|
+
let dashboard: string[];
|
|
6
|
+
let login: string[];
|
|
7
|
+
let signup: string[];
|
|
8
|
+
let payment: string[];
|
|
9
|
+
let pricing: string[];
|
|
10
|
+
let chat: string[];
|
|
11
|
+
let settings: string[];
|
|
12
|
+
let calendar: string[];
|
|
13
|
+
let table: string[];
|
|
14
|
+
let gallery: string[];
|
|
15
|
+
let hero: string[];
|
|
16
|
+
let form: string[];
|
|
17
|
+
let profile: string[];
|
|
18
|
+
let documentation: string[];
|
|
19
|
+
let navigation: string[];
|
|
20
|
+
}
|
|
21
|
+
export const SYNONYM_INDEX: Map<string, Set<string>>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Search's synonym map: product words an agent types, mapped to the
|
|
5
|
+
* catalog's vocabulary in both directions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Synonym / intent map: product-language terms an agent is likely to type,
|
|
10
|
+
* expanded to the catalog's vocabulary so oblique queries still rank. Keys and
|
|
11
|
+
* values are matched bidirectionally (typing any value also pulls in the key
|
|
12
|
+
* and its siblings). Lowercase, single words or short phrases. Exported for
|
|
13
|
+
* `build`, whose page ranker expands a query with the same vocabulary.
|
|
14
|
+
*/
|
|
15
|
+
export const SYNONYMS = {
|
|
16
|
+
dashboard: [
|
|
17
|
+
'overview',
|
|
18
|
+
'analytics',
|
|
19
|
+
'kpi',
|
|
20
|
+
'kpis',
|
|
21
|
+
'metrics',
|
|
22
|
+
'stats',
|
|
23
|
+
'reporting',
|
|
24
|
+
'insights',
|
|
25
|
+
'control',
|
|
26
|
+
],
|
|
27
|
+
login: ['signin', 'auth', 'authentication', 'sso', 'credentials', 'account'],
|
|
28
|
+
signup: ['register', 'registration', 'onboarding'],
|
|
29
|
+
payment: ['checkout', 'billing', 'card', 'pay', 'purchase', 'order'],
|
|
30
|
+
pricing: ['plans', 'plan', 'tiers', 'tier', 'subscription', 'subscriptions'],
|
|
31
|
+
chat: ['messaging', 'message', 'messages', 'conversation', 'inbox', 'dm'],
|
|
32
|
+
settings: ['preferences', 'config', 'configuration', 'account'],
|
|
33
|
+
calendar: ['schedule', 'scheduling', 'events', 'event', 'month', 'agenda'],
|
|
34
|
+
table: ['list', 'rows', 'records', 'grid', 'spreadsheet', 'datatable'],
|
|
35
|
+
gallery: ['photos', 'photo', 'images', 'image', 'pictures'],
|
|
36
|
+
hero: ['banner', 'splash', 'headline', 'landing'],
|
|
37
|
+
form: ['fields', 'input', 'inputs', 'survey'],
|
|
38
|
+
profile: ['bio', 'avatar', 'user'],
|
|
39
|
+
documentation: ['docs', 'reference', 'guide', 'api'],
|
|
40
|
+
navigation: ['nav', 'menu', 'sidebar'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Flatten into a token -> Set(expansions) lookup (bidirectional).
|
|
44
|
+
export const SYNONYM_INDEX = (() => {
|
|
45
|
+
/** @type {Map<string, Set<string>>} */
|
|
46
|
+
const idx = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} a
|
|
49
|
+
* @param {string} b
|
|
50
|
+
*/
|
|
51
|
+
const add = (a, b) => {
|
|
52
|
+
let set = idx.get(a);
|
|
53
|
+
if (!set) {
|
|
54
|
+
set = new Set();
|
|
55
|
+
idx.set(a, set);
|
|
56
|
+
}
|
|
57
|
+
set.add(b);
|
|
58
|
+
};
|
|
59
|
+
for (const [key, vals] of Object.entries(SYNONYMS)) {
|
|
60
|
+
for (const v of vals) {
|
|
61
|
+
add(key, v);
|
|
62
|
+
add(v, key);
|
|
63
|
+
for (const v2 of vals) if (v2 !== v) add(v, v2);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return idx;
|
|
67
|
+
})();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
export const template: import("../../foundation/route/route.mjs").Searcher<import("./tree.mjs").SearchRequest, import("./_adapter.mjs").SearchCatalog, import("./search.type.mjs").SearchResponse>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The template searcher: the main searcher's signals with its own
|
|
5
|
+
* weights, in _template-searcher.weights.json.
|
|
6
|
+
* @input {query, options} and the gathered catalog with its static data
|
|
7
|
+
* @output the search response, the lexical response's shape and size
|
|
8
|
+
* @position api/search; a topic child of the root dispatcher in tree.mjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {routeKit} from '../../foundation/route/route.mjs';
|
|
12
|
+
import {asWeights, rankWith} from './_main-searcher.mjs';
|
|
13
|
+
|
|
14
|
+
/** @type {import('./tree.mjs').SearchKit} */
|
|
15
|
+
const {defineSearcher} = routeKit;
|
|
16
|
+
|
|
17
|
+
export const template = defineSearcher({
|
|
18
|
+
id: 'template',
|
|
19
|
+
summary:
|
|
20
|
+
'Ranks entries for queries that ask for a template, with weights of its own.',
|
|
21
|
+
run: (request, catalog, weights) =>
|
|
22
|
+
rankWith(request, catalog, asWeights(weights) ?? catalog.data.template),
|
|
23
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Tests for the template searcher (_template-searcher.mjs): with its own weights
|
|
5
|
+
* it ranks a template first for a query that asks for one. Run against the real
|
|
6
|
+
* @astryxdesign/core.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {describe, it, expect} from 'vitest';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import {fileURLToPath} from 'node:url';
|
|
12
|
+
import {gatherCandidates, loadSearchData} from './_adapter.mjs';
|
|
13
|
+
import {template} from './_template-searcher.mjs';
|
|
14
|
+
|
|
15
|
+
const REPO = path.resolve(
|
|
16
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
+
'../../../..',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
describe('template searcher', () => {
|
|
21
|
+
it('ranks a template near the top for a template query', async () => {
|
|
22
|
+
const catalog = {
|
|
23
|
+
...(await gatherCandidates({cwd: REPO})),
|
|
24
|
+
data: loadSearchData(),
|
|
25
|
+
};
|
|
26
|
+
const answer = /** @type {any} */ (
|
|
27
|
+
await template.run(
|
|
28
|
+
{query: 'page of settings with a side menu', options: {cwd: REPO}},
|
|
29
|
+
catalog,
|
|
30
|
+
null,
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
expect(
|
|
34
|
+
answer.data.results
|
|
35
|
+
.slice(0, 3)
|
|
36
|
+
.some((/** @type {any} */ r) => r.domain === 'template'),
|
|
37
|
+
).toBe(true);
|
|
38
|
+
expect(answer.data.results.length).toBeLessThanOrEqual(20);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"w":[0.26388,-0.013032,0.516431,0.121331,0.254512,-0.261785,-0.688666,0.112076,-0.376705,0.111327,0.097715,0.065046,0.095694,-0.016049,-0.236365,0.094383,0.0,-0.003606,0.003684,0.067567,0.024298,-0.030137,0.324971,-0.059765,-0.008739,0.015668,0.011897,0.016649,-0.066041,0.056844,-0.055073,0.054001,0.137491,0.0,0.002283,-0.360264,0.210105,0.137841,0.14848,0.051643,0.401352,0.0,0.0,0.0,0.17537,-0.278616,0.138193,0.041566,0.02038,0.043262,0.134831,0.272071,-0.024414,-0.000472,0.013668,0.0,0.034693,0.004465,0.0],"mu":[0.164885,0.00511,0.225783,0.017021,0.147234,0.050213,0.178723,0.617872,0.005957,0.260431,0.758487,0.118373,0.062975,0.000497,0.08043,0.415077,0.054719,0.001375,4e-06,0.016249,0.004739,0.048162,0.095679,5.7e-05,0.00111,0.000198,0.001298,0.002502,1e-06,0.022962,0.00435,0.075387,0.157731,0.0,0.132838,0.026582,0.222958,0.37547,0.00064,0.098856,0.019517,0.0,0.0,0.0,0.014202,0.002142,0.017938,0.028668,2.6e-05,0.030184,0.001323,0.02033,0.363165,7.4e-05,0.000775,0.0,0.000152,0.000448,0.0],"sd":[0.30267,0.037002,0.399695,0.129352,0.35434,0.218385,0.383122,0.485909,0.076955,1.016246,1.780214,0.698764,0.119979,0.022138,0.262291,0.251643,1e-06,0.037042,0.002117,0.107524,0.058464,0.180391,0.248041,0.00567,0.021262,0.005551,0.018508,0.023617,0.00015,0.335998,0.139746,0.538946,0.804633,1e-06,0.783314,0.348833,1.042717,1.321848,0.07162,0.646141,0.275325,1e-06,1e-06,1e-06,0.066632,0.023566,0.076235,0.079854,0.002538,0.085732,0.008484,0.056206,0.296331,0.001967,0.027817,1e-06,0.012337,0.021154,1e-06]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The first title or heading that holds every word of the query, in order and
|
|
6
|
+
* side by side, or null.
|
|
7
|
+
* @param {string} term - Lowercased full query.
|
|
8
|
+
* @param {string[] | undefined} titles
|
|
9
|
+
* @returns {string | null}
|
|
10
|
+
*/
|
|
11
|
+
export function headingWithPhrase(term: string, titles: string[] | undefined): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* The first title or heading of two words or more that the query holds whole,
|
|
14
|
+
* in order and side by side, or null. A question such as "how do I add dark
|
|
15
|
+
* mode" names the "Dark mode" section outright, around words no title has.
|
|
16
|
+
* @param {string} term - Lowercased full query.
|
|
17
|
+
* @param {string[] | undefined} titles
|
|
18
|
+
* @returns {string | null}
|
|
19
|
+
*/
|
|
20
|
+
export function titleInQuery(term: string, titles: string[] | undefined): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The score of a whole-query phrase inside a doc's title or heading: a keyword
|
|
23
|
+
* substring hit (70) promoted by the same 100 as the exact tier. Below an
|
|
24
|
+
* exact name or keyword (190-200), above the token-sum path (~151 at most).
|
|
25
|
+
*/
|
|
26
|
+
export const TITLE_PHRASE_SCORE: 170;
|
|
27
|
+
/**
|
|
28
|
+
* The score of a whole title inside a longer query, before its coverage bonus:
|
|
29
|
+
* one step below {@link TITLE_PHRASE_SCORE}. The bonus (one per query term the
|
|
30
|
+
* candidate matches, at most 9) orders the sections that share a common title,
|
|
31
|
+
* so "best practices for spacing" puts Spacing's Best Practices first.
|
|
32
|
+
*/
|
|
33
|
+
export const TITLE_IN_QUERY_SCORE: 160;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The title tier of a multi-word query: the whole query as a phrase in
|
|
5
|
+
* a doc's title or heading, or a whole title inside the query.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {phraseWords, samePhraseWord} from './_query.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The score of a whole-query phrase inside a doc's title or heading: a keyword
|
|
12
|
+
* substring hit (70) promoted by the same 100 as the exact tier. Below an
|
|
13
|
+
* exact name or keyword (190-200), above the token-sum path (~151 at most).
|
|
14
|
+
*/
|
|
15
|
+
export const TITLE_PHRASE_SCORE = 170;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The score of a whole title inside a longer query, before its coverage bonus:
|
|
19
|
+
* one step below {@link TITLE_PHRASE_SCORE}. The bonus (one per query term the
|
|
20
|
+
* candidate matches, at most 9) orders the sections that share a common title,
|
|
21
|
+
* so "best practices for spacing" puts Spacing's Best Practices first.
|
|
22
|
+
*/
|
|
23
|
+
export const TITLE_IN_QUERY_SCORE = 160;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The first title or heading that holds every word of the query, in order and
|
|
27
|
+
* side by side, or null.
|
|
28
|
+
* @param {string} term - Lowercased full query.
|
|
29
|
+
* @param {string[] | undefined} titles
|
|
30
|
+
* @returns {string | null}
|
|
31
|
+
*/
|
|
32
|
+
export function headingWithPhrase(term, titles) {
|
|
33
|
+
const query = phraseWords(term);
|
|
34
|
+
if (query.length < 2 || !titles) return null;
|
|
35
|
+
for (const title of titles) {
|
|
36
|
+
const words = phraseWords(String(title ?? ''));
|
|
37
|
+
for (let i = 0; i + query.length <= words.length; i++) {
|
|
38
|
+
if (query.every((word, j) => samePhraseWord(words[i + j], word)))
|
|
39
|
+
return title;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The first title or heading of two words or more that the query holds whole,
|
|
47
|
+
* in order and side by side, or null. A question such as "how do I add dark
|
|
48
|
+
* mode" names the "Dark mode" section outright, around words no title has.
|
|
49
|
+
* @param {string} term - Lowercased full query.
|
|
50
|
+
* @param {string[] | undefined} titles
|
|
51
|
+
* @returns {string | null}
|
|
52
|
+
*/
|
|
53
|
+
export function titleInQuery(term, titles) {
|
|
54
|
+
const query = phraseWords(term);
|
|
55
|
+
if (!titles) return null;
|
|
56
|
+
for (const title of titles) {
|
|
57
|
+
const words = phraseWords(String(title ?? ''));
|
|
58
|
+
if (words.length < 2 || words.length > query.length) continue;
|
|
59
|
+
for (let i = 0; i + words.length <= query.length; i++) {
|
|
60
|
+
if (words.every((word, j) => samePhraseWord(query[i + j], word)))
|
|
61
|
+
return title;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
|
+
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @file Word matching for search: stems and word forms, word starts,
|
|
6
|
+
* identifier spelling, joined names, and typo tolerance.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Light stemmer: strips common English suffixes so "charts"/"charting" and
|
|
10
|
+
* "chart" share a root. Deliberately crude (no Porter) — good enough to bridge
|
|
11
|
+
* plural/gerund gaps without a dependency.
|
|
12
|
+
* @param {string} w
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function stem(w: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* The forms of a word that count as the same word: itself, its stem, and its
|
|
18
|
+
* singular when it ends in a plural suffix — so "tables" is "table",
|
|
19
|
+
* "statuses" is "status", and "filtering" is "filter".
|
|
20
|
+
* @param {string} w - Lowercase word.
|
|
21
|
+
* @returns {Set<string>}
|
|
22
|
+
*/
|
|
23
|
+
export function wordForms(w: string): Set<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Whether two lowercase words are the same word, up to plural and stem form.
|
|
26
|
+
* @param {string} a
|
|
27
|
+
* @param {string} b
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
export function sameWord(a: string, b: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The lowercase words of a name or keyword: split at non-alphanumerics and at
|
|
33
|
+
* camelCase boundaries, so "TextInput" is ["text", "input"] and
|
|
34
|
+
* "Dashboard - Analytics" is ["dashboard", "analytics"].
|
|
35
|
+
* @param {string} text
|
|
36
|
+
* @returns {string[]}
|
|
37
|
+
*/
|
|
38
|
+
export function wordsOf(text: string): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Whether a term is found inside a name or keyword: it is one of its words, or
|
|
41
|
+
* the start of one (a truncation), and covers at least half of the whole
|
|
42
|
+
* string. Four letters minimum, so a short term never matches by accident.
|
|
43
|
+
* @param {string} term - Lowercase term.
|
|
44
|
+
* @param {string} text - The name or keyword as authored.
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function startsAWordOf(term: string, text: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* A query with its camelCase and PascalCase identifiers spelled as words, so
|
|
50
|
+
* `SidebarToggle` reads "sidebar toggle" and `useTablePaginationState` "use
|
|
51
|
+
* table pagination state". Plain words, and identifiers joined with `_` or `-`, are
|
|
52
|
+
* unchanged: a joined token that matches nothing stays unmatched.
|
|
53
|
+
* @param {string} query - The query as typed, before lowercasing.
|
|
54
|
+
* @returns {string} Lowercased.
|
|
55
|
+
*/
|
|
56
|
+
export function spellIdentifiers(query: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* A name or keyword with its separators removed, lowercased: `Side Nav`,
|
|
59
|
+
* `side-nav`, and `SideNav` all join to `sidenav`.
|
|
60
|
+
* @param {string} text
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function joined(text: string): string;
|
|
64
|
+
export function isTypo(a: string, b: string, dist: number): boolean;
|