@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,351 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The lexical ranker: keyword and fuzzy scoring of every gathered
|
|
5
|
+
* candidate, as one pure function (`rankLexical`). `searchCore` (search.mjs)
|
|
6
|
+
* runs it alone, and search's searchers read its ranking. No I/O: the adapter
|
|
7
|
+
* gathers the candidates. Word handling lives in _words.mjs, _synonyms.mjs and
|
|
8
|
+
* _query.mjs; a candidate's score in _score.mjs and _candidate-score.mjs.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* Returns the same typed envelope { type, data } that `xds --json search`
|
|
12
|
+
* outputs. The CLI command handler is a thin wrapper around this function.
|
|
13
|
+
*
|
|
14
|
+
* Scoring is keyword + fuzzy ranking; the per-term signal weights are
|
|
15
|
+
* listed in _candidate-score.mjs.
|
|
16
|
+
*
|
|
17
|
+
* A multi-word query has a reserved top tier (see {@link scoreQuery}): the
|
|
18
|
+
* whole query as a candidate's name or keyword (190-200), then the whole query
|
|
19
|
+
* as a phrase inside a doc's title or one of its headings (170), then a whole
|
|
20
|
+
* title of two words or more inside the query (160-169), then a candidate that
|
|
21
|
+
* matches every word of the query, at least one of them by name or keyword
|
|
22
|
+
* (151-159). Below those sits everything else: a partial match, or every word
|
|
23
|
+
* matched only in prose or through the components a page renders. A section titled "Light/Dark Mode" answers `dark
|
|
24
|
+
* mode` better than any doc that merely names `mode` in code, however exactly;
|
|
25
|
+
* "Dark mode" answers `how do I add dark mode`; and a guide whose title and
|
|
26
|
+
* description hold both words of `troubleshoot integration` answers it better
|
|
27
|
+
* than a doc named `integration`.
|
|
28
|
+
*
|
|
29
|
+
* Between domains, ranking adds one rule ahead of the score: domain priority
|
|
30
|
+
* (see {@link domainPriority}). A component, hook, template, or theme whose
|
|
31
|
+
* name or keyword a query word hits ranks ahead of every doc the reader did not
|
|
32
|
+
* ask for by name, title, or a declared phrase, so `font size` finds `Text`
|
|
33
|
+
* before a token page whose heading holds the phrase. A doc the query names,
|
|
34
|
+
* whose title the query holds, or that declares the whole query as a keyword
|
|
35
|
+
* keeps its place.
|
|
36
|
+
*
|
|
37
|
+
* Description and guidance are separate tiers on purpose. A component's own
|
|
38
|
+
* one-line description saying "notification" is a claim about what it IS; the
|
|
39
|
+
* same word inside another component's best-practice advice is a passing
|
|
40
|
+
* mention. Scored equally, `Toast` — "a brief, non-blocking notification" —
|
|
41
|
+
* ties with `Card`, `Dialog` and `Item`, which merely mention notifications in
|
|
42
|
+
* their guidance, and ties break alphabetically, so Toast falls off the end of
|
|
43
|
+
* its own best query.
|
|
44
|
+
*
|
|
45
|
+
* `keywords` carry AUTHORED intent — a block's `componentsUsed`, a page's
|
|
46
|
+
* `category` words. `weakKeywords` are DERIVED: the components a page template
|
|
47
|
+
* happens to render, scraped out of its JSX. Derived signal is deliberately
|
|
48
|
+
* capped below the confident-match gate on its own, because breadth is not
|
|
49
|
+
* relevance. At full keyword strength every rendered component is an
|
|
50
|
+
* independent 90-point shot with no penalty for how many a template has, so
|
|
51
|
+
* the broadest pages (a theme showcase rendering one of everything) win
|
|
52
|
+
* queries they have nothing to do with.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import {spellIdentifiers, joined} from './_words.mjs';
|
|
56
|
+
import {tokenizeQuery, phraseWords} from './_query.mjs';
|
|
57
|
+
import {MAX_RUN_WORDS} from './_compound.mjs';
|
|
58
|
+
import {scoreQuery} from './_score.mjs';
|
|
59
|
+
import {scoreCandidate} from './_candidate-score.mjs';
|
|
60
|
+
import {domainPriority} from './_priority.mjs';
|
|
61
|
+
import {toResult, searchedComponentsOf} from './_result.mjs';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A search candidate gathered from one content domain. Extra underscore-
|
|
65
|
+
* prefixed fields carry domain-specific payload used only by {@link toResult}.
|
|
66
|
+
* @typedef {object} Candidate
|
|
67
|
+
* @property {'component'|'hook'|'doc'|'template'|'theme'} domain
|
|
68
|
+
* @property {string} name
|
|
69
|
+
* @property {string[]} [aliases] - Other names the candidate answers to, scored
|
|
70
|
+
* with the same name signals: a theme's display name.
|
|
71
|
+
* @property {string[]} [keywords]
|
|
72
|
+
* @property {string[]} [_declared] - A doc's own declared keywords: what its
|
|
73
|
+
* author lists in `keywords`, without the code terms and table cells its
|
|
74
|
+
* other keywords add.
|
|
75
|
+
* @property {string[]} [weakKeywords]
|
|
76
|
+
* @property {string} [description]
|
|
77
|
+
* @property {string[]} [prose]
|
|
78
|
+
* @property {string[]} [guidance]
|
|
79
|
+
* @property {string[]} [titles] - A doc's title and the headings inside it:
|
|
80
|
+
* the lines a reader scans to pick it. The whole query standing in one of
|
|
81
|
+
* them, or one of them standing whole in the query, is a top-tier match.
|
|
82
|
+
* @property {string} [_import]
|
|
83
|
+
* @property {string} [_title]
|
|
84
|
+
* @property {string} [_topic] - A doc result's topic or docs-tree route.
|
|
85
|
+
* @property {string} [_section] - A doc result's section key, when it is one section.
|
|
86
|
+
* @property {string} [_command] - The command that reads exactly this doc part.
|
|
87
|
+
* @property {string} [_parent] - The command that opens the level above a doc
|
|
88
|
+
* part: its topic's section list, or the docs-tree namespace it sits in.
|
|
89
|
+
* @property {string} [_package] - The npm package that owns the candidate:
|
|
90
|
+
* Core's package for its components, hooks, and templates, the integration's
|
|
91
|
+
* for what it contributed, and for a doc part the package that wrote it.
|
|
92
|
+
* @property {string} [_displayName]
|
|
93
|
+
* @property {'page'|'block'} [_kind]
|
|
94
|
+
* @property {string} [_resultName]
|
|
95
|
+
* @property {string} [_commandName]
|
|
96
|
+
* @property {'file'|'doc'} [_hold] - How an integration component holds its
|
|
97
|
+
* name, for the owner rule.
|
|
98
|
+
* @property {string} [_docPath] - An integration component's doc.
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* True when the query is the candidate's whole name or another name of it,
|
|
103
|
+
* typed as is or, for anything but a doc, with its spaces removed. Such a
|
|
104
|
+
* query names that entry outright.
|
|
105
|
+
* @param {Candidate} candidate
|
|
106
|
+
* @param {string} term the query, trimmed and lowercased
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
export function namesWhole(candidate, term) {
|
|
110
|
+
return [candidate.name, ...(candidate.aliases ?? [])].some(name => {
|
|
111
|
+
const lower = String(name).toLowerCase();
|
|
112
|
+
return (
|
|
113
|
+
lower === term ||
|
|
114
|
+
(candidate.domain !== 'doc' &&
|
|
115
|
+
!/[\s_-]/.test(lower) &&
|
|
116
|
+
lower === term.replace(/\s+/g, ''))
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Today's lexical ranking of a gathered catalog: the search response
|
|
123
|
+
* `searchCore` returns.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} query
|
|
126
|
+
* @param {{limit?: number}} options
|
|
127
|
+
* @param {{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside?: boolean, coreSkipped?: string | null}} catalog
|
|
128
|
+
* @returns {import('./search.type.mjs').SearchResponse}
|
|
129
|
+
*/
|
|
130
|
+
export function rankLexical(query, options, catalog) {
|
|
131
|
+
const {limit = 20} = options;
|
|
132
|
+
const term = String(query).trim().toLowerCase();
|
|
133
|
+
const {all, components} = catalog;
|
|
134
|
+
|
|
135
|
+
// A query that is one identifier counts as the words it spells
|
|
136
|
+
// (`SidebarToggle`), and still matches whole as typed. In a query of several
|
|
137
|
+
// words, each identifier stays one term (`ChatComposer ChatLayout` names two
|
|
138
|
+
// components), and its words still join into names (see compoundHit). Both
|
|
139
|
+
// only help a query that names no entry outright: when the query is an
|
|
140
|
+
// entry's name, typed whole or as its words, that entry answers it, and
|
|
141
|
+
// neither reading may add near siblings beside it.
|
|
142
|
+
const spelled = spellIdentifiers(String(query).trim());
|
|
143
|
+
const exact = all.some(candidate => namesWhole(candidate, term));
|
|
144
|
+
// One identifier is read as its words only when, read whole, it matches
|
|
145
|
+
// nothing: a prop or hook name a doc already mentions keeps the
|
|
146
|
+
// entries that mention it.
|
|
147
|
+
const split =
|
|
148
|
+
!exact &&
|
|
149
|
+
!/\s/.test(term) &&
|
|
150
|
+
spelled !== term &&
|
|
151
|
+
!all.some(candidate => scoreCandidate(term, candidate));
|
|
152
|
+
const tokens = tokenizeQuery(split ? spelled : term);
|
|
153
|
+
/** @type {string[]} */
|
|
154
|
+
const words = exact
|
|
155
|
+
? []
|
|
156
|
+
: phraseWords(split || /\s/.test(term) ? spelled : term);
|
|
157
|
+
// The entries the query names: a component, hook, or template whose whole
|
|
158
|
+
// name is adjacent query words joined (`icon button in a card header` names
|
|
159
|
+
// IconButton), or one identifier typed as such. A name inside a longer named
|
|
160
|
+
// run does not count (`icon` there is part of IconButton).
|
|
161
|
+
const queryWords = phraseWords(spelled);
|
|
162
|
+
const typedIdentifiers = new Set(
|
|
163
|
+
(String(query).match(/[A-Za-z0-9]+/g) ?? [])
|
|
164
|
+
.filter(w => /[A-Z]/.test(w))
|
|
165
|
+
.map(w => w.toLowerCase()),
|
|
166
|
+
);
|
|
167
|
+
/** @type {Map<string, [number, number]>} each run joined, longest first */
|
|
168
|
+
const spans = new Map();
|
|
169
|
+
for (let len = Math.min(queryWords.length, MAX_RUN_WORDS); len >= 1; len--) {
|
|
170
|
+
for (let a = 0; a + len <= queryWords.length; a++) {
|
|
171
|
+
// One plain word names too much (`list`, `field`); a named entry is a
|
|
172
|
+
// run of joined words, or an identifier typed as one.
|
|
173
|
+
if (len === 1 && !typedIdentifiers.has(queryWords[a])) continue;
|
|
174
|
+
const run = queryWords.slice(a, a + len).join('');
|
|
175
|
+
if (!spans.has(run)) spans.set(run, [a, a + len]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Where a query word is the candidate's whole name, or null: such an entry
|
|
180
|
+
* keeps the rank its name earns, whatever else it matched.
|
|
181
|
+
* @param {Candidate} candidate
|
|
182
|
+
* @returns {[number, number] | null}
|
|
183
|
+
*/
|
|
184
|
+
const wordNamed = candidate => {
|
|
185
|
+
if (candidate.domain === 'doc') return null;
|
|
186
|
+
for (const name of [candidate.name, ...(candidate.aliases ?? [])]) {
|
|
187
|
+
const n = joined(name);
|
|
188
|
+
const at = queryWords.findIndex(w => w === n || w === `${n}s`);
|
|
189
|
+
if (at >= 0) return [at, at + 1];
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
};
|
|
193
|
+
/** @param {Candidate} candidate @returns {[number, number] | null} */
|
|
194
|
+
const namedSpan = candidate => {
|
|
195
|
+
if (candidate.domain === 'doc') return null;
|
|
196
|
+
/** @type {[number, number] | null} */
|
|
197
|
+
let best = null;
|
|
198
|
+
for (const name of [candidate.name, ...(candidate.aliases ?? [])]) {
|
|
199
|
+
const n = joined(name);
|
|
200
|
+
const span =
|
|
201
|
+
spans.get(n) ??
|
|
202
|
+
spans.get(`${n}s`) ??
|
|
203
|
+
(/(?:s|x|z|ch|sh)$/.test(n) ? spans.get(`${n}es`) : undefined);
|
|
204
|
+
if (span && (!best || span[1] - span[0] > best[1] - best[0])) {
|
|
205
|
+
best = span;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return best;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Score every candidate on its own merits. The consumer groups results by
|
|
212
|
+
// role (page / block / component) and takes the top of each, so there's no
|
|
213
|
+
// cross-role competition to engineer — a target page only needs to be the
|
|
214
|
+
// strongest PAGE, not outrank every component.
|
|
215
|
+
/** @type {{result: any, priority: 0 | 1 | 2, key: number, alias: 'partial' | 'keyword' | false, aliasWords: number, candidate: Candidate, span: [number, number] | null, word: [number, number] | null}[]} */
|
|
216
|
+
const ranked = [];
|
|
217
|
+
for (const candidate of all) {
|
|
218
|
+
const hit = scoreQuery(term, tokens, candidate, words);
|
|
219
|
+
if (hit)
|
|
220
|
+
ranked.push({
|
|
221
|
+
result: toResult(
|
|
222
|
+
candidate,
|
|
223
|
+
hit.score,
|
|
224
|
+
hit.reason,
|
|
225
|
+
hit.matched,
|
|
226
|
+
hit.total,
|
|
227
|
+
catalog.outside,
|
|
228
|
+
),
|
|
229
|
+
priority: domainPriority(term, tokens, candidate, hit, words),
|
|
230
|
+
key: hit.score,
|
|
231
|
+
alias: hit.alias ?? false,
|
|
232
|
+
aliasWords: hit.aliasWords ?? 0,
|
|
233
|
+
candidate,
|
|
234
|
+
span: namedSpan(candidate),
|
|
235
|
+
word: wordNamed(candidate),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// A match on a keyword read joined never outranks an entry the query names,
|
|
239
|
+
// and neither does a match on part of another name unless it spans more
|
|
240
|
+
// query words than any named run: `table header with sticky columns` keeps
|
|
241
|
+
// TableHeader above useTableStickyColumns, which only holds the words
|
|
242
|
+
// `sticky columns`. Only the order changes; each result keeps the score its
|
|
243
|
+
// text earned.
|
|
244
|
+
/** @param {[number, number]} inner @param {[number, number]} outer */
|
|
245
|
+
const inside = (inner, outer) =>
|
|
246
|
+
outer[0] <= inner[0] &&
|
|
247
|
+
outer[1] >= inner[1] &&
|
|
248
|
+
outer[1] - outer[0] > inner[1] - inner[0];
|
|
249
|
+
const named = ranked.filter(r => {
|
|
250
|
+
const span = r.span;
|
|
251
|
+
return (
|
|
252
|
+
span != null &&
|
|
253
|
+
!ranked.some(o => o !== r && o.span != null && inside(span, o.span))
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
/** @param {{key: number}[]} rows */
|
|
257
|
+
const floorOf = rows =>
|
|
258
|
+
rows.length ? Math.min(...rows.map(r => r.key)) : Infinity;
|
|
259
|
+
const namedFloor = floorOf(named);
|
|
260
|
+
const longestNamed = Math.max(
|
|
261
|
+
0,
|
|
262
|
+
...named.map(r => (r.span ? r.span[1] - r.span[0] : 0)),
|
|
263
|
+
);
|
|
264
|
+
for (const r of ranked) {
|
|
265
|
+
if (named.includes(r)) continue;
|
|
266
|
+
if (!r.alias || r.word) continue;
|
|
267
|
+
// A match on part of a longer name that spans more words than any named
|
|
268
|
+
// run reads more of the query than the named entry does (`dropdown menu
|
|
269
|
+
// radio` and DropdownMenuRadioItem, against DropdownMenu), so it is not
|
|
270
|
+
// held back. A joined keyword always is.
|
|
271
|
+
if (r.alias === 'partial' && r.aliasWords > longestNamed) continue;
|
|
272
|
+
// It keeps what it earns without those matches.
|
|
273
|
+
const own =
|
|
274
|
+
scoreQuery(term, tokens, r.candidate, words, {plain: true})?.score ?? 0;
|
|
275
|
+
r.key = Math.max(own, Math.min(r.key, namedFloor - 0.5));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Docs and everything else are each sorted by score desc, then domain
|
|
279
|
+
// (stable order), then name. The two are merged: at each step the stronger
|
|
280
|
+
// head goes next, except that a head with domain priority goes ahead of a
|
|
281
|
+
// doc head without it (see domainPriority). Each side keeps its own order.
|
|
282
|
+
/** @type {Record<string, number>} */
|
|
283
|
+
const domainOrder = {component: 0, hook: 1, doc: 2, template: 3, theme: 4};
|
|
284
|
+
/**
|
|
285
|
+
* @param {{result: any, key: number}} x
|
|
286
|
+
* @param {{result: any, key: number}} y
|
|
287
|
+
*/
|
|
288
|
+
const byScore = ({result: a, key: ka}, {result: b, key: kb}) =>
|
|
289
|
+
kb - ka ||
|
|
290
|
+
(domainOrder[a.domain] ?? 9) - (domainOrder[b.domain] ?? 9) ||
|
|
291
|
+
a.name.localeCompare(b.name);
|
|
292
|
+
const others = ranked.filter(r => r.result.domain !== 'doc').sort(byScore);
|
|
293
|
+
// Against other domains, a doc the reader asked for competes ahead of docs
|
|
294
|
+
// they did not, so it is not held behind a broad page that outscores it.
|
|
295
|
+
// Docs alone keep their text-match order, with a doc the whole query names
|
|
296
|
+
// first.
|
|
297
|
+
const docs = ranked
|
|
298
|
+
.filter(r => r.result.domain === 'doc')
|
|
299
|
+
.sort((x, y) =>
|
|
300
|
+
others.length > 0
|
|
301
|
+
? y.priority - x.priority || byScore(x, y)
|
|
302
|
+
: Number(y.priority === 2) - Number(x.priority === 2) || byScore(x, y),
|
|
303
|
+
);
|
|
304
|
+
const scored = [];
|
|
305
|
+
let i = 0;
|
|
306
|
+
let j = 0;
|
|
307
|
+
while (i < others.length || j < docs.length) {
|
|
308
|
+
const other = others[i];
|
|
309
|
+
const doc = docs[j];
|
|
310
|
+
const otherFirst =
|
|
311
|
+
!doc ||
|
|
312
|
+
(other != null &&
|
|
313
|
+
((other.priority && !doc.priority) || byScore(other, doc) < 0));
|
|
314
|
+
scored.push(otherFirst ? others[i++].result : docs[j++].result);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// `results` is bounded by `limit` so a caller (and the recorded run that
|
|
318
|
+
// quotes it) never carries an unbounded payload. `matchCount` is the number
|
|
319
|
+
// of matches that bound was applied TO — reporting `results.length` there
|
|
320
|
+
// would report the cap back as if it were the answer, so a query matching
|
|
321
|
+
// 57 things and one matching exactly 20 would be indistinguishable.
|
|
322
|
+
const limited = scored.slice(0, limit);
|
|
323
|
+
|
|
324
|
+
/** @type {import('./search.type.mjs').SearchResponse} */
|
|
325
|
+
const response = {
|
|
326
|
+
type: 'search',
|
|
327
|
+
data: {
|
|
328
|
+
query: String(query).trim(),
|
|
329
|
+
matchCount: scored.length,
|
|
330
|
+
// toResult gives every domain its command and domain fields.
|
|
331
|
+
results: /** @type {import('./search.type.mjs').SearchResultEntry[]} */ (
|
|
332
|
+
limited
|
|
333
|
+
),
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
if (catalog.coreSkipped) {
|
|
337
|
+
response.meta = {
|
|
338
|
+
partial: {
|
|
339
|
+
skipped: ['component', 'hook', 'template'],
|
|
340
|
+
reason: catalog.coreSkipped,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (catalog.searchedComponents) {
|
|
345
|
+
searchedComponentsOf.set(
|
|
346
|
+
response,
|
|
347
|
+
components.map(c => ({name: c.name, keywords: c.keywords ?? []})),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return response;
|
|
351
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
* Ranks with one searcher's weights. Shared by the main searcher and every
|
|
6
|
+
* topic searcher, which differ only in their weights.
|
|
7
|
+
* @param {import('./tree.mjs').SearchRequest} request
|
|
8
|
+
* @param {import('./tree.mjs').SearchCatalog} catalog
|
|
9
|
+
* @param {SearcherWeights} weights
|
|
10
|
+
* @returns {import('./search.type.mjs').SearchResponse}
|
|
11
|
+
*/
|
|
12
|
+
export function rankWith(request: import("./tree.mjs").SearchRequest, catalog: import("./tree.mjs").SearchCatalog, weights: SearcherWeights): import("./search.type.mjs").SearchResponse;
|
|
13
|
+
export function asWeights(weights: unknown): SearcherWeights | null;
|
|
14
|
+
export const main: import("../../foundation/route/route.mjs").Searcher<import("./tree.mjs").SearchRequest, import("./_adapter.mjs").SearchCatalog, import("./search.type.mjs").SearchResponse>;
|
|
15
|
+
export type SearcherWeights = {
|
|
16
|
+
w: number[];
|
|
17
|
+
mu: number[];
|
|
18
|
+
sd: number[];
|
|
19
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The main searcher: ranks every entry by a weighted sum of plain
|
|
5
|
+
* signals (see _features.mjs): the lexical ranker's score and rank, BM25 over
|
|
6
|
+
* each entry's name, docs text and source-built card, query-word coverage,
|
|
7
|
+
* whether the query holds the entry's name, and the router's guess of the
|
|
8
|
+
* domain the query wants. Its weights are static numbers in
|
|
9
|
+
* _main-searcher.weights.json.
|
|
10
|
+
* @input {query, options} and the gathered catalog with its static data
|
|
11
|
+
* @output the search response, the lexical response's shape and size
|
|
12
|
+
* @position api/search; a child of the root dispatcher in tree.mjs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {routeKit} from '../../foundation/route/route.mjs';
|
|
16
|
+
import {rank, signals} from './_features.mjs';
|
|
17
|
+
import {entryTable} from './_entries.mjs';
|
|
18
|
+
import {orderMatches} from './_order.mjs';
|
|
19
|
+
import {routeProbs} from './_query-words.mjs';
|
|
20
|
+
import {namesWhole, rankLexical} from './_lexical.mjs';
|
|
21
|
+
|
|
22
|
+
/** @typedef {{w: number[], mu: number[], sd: number[]}} SearcherWeights */
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A tuning override for a searcher, or null for its shipped weights.
|
|
26
|
+
* @param {unknown} weights
|
|
27
|
+
* @returns {SearcherWeights | null}
|
|
28
|
+
*/
|
|
29
|
+
export const asWeights = weights =>
|
|
30
|
+
/** @type {SearcherWeights | null} */ (weights);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A name read loosely: lowercase, with dashes, underscores and slashes read as
|
|
34
|
+
* spaces.
|
|
35
|
+
* @param {string} text
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
const looseName = text =>
|
|
39
|
+
text
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[-_/]+/g, ' ')
|
|
42
|
+
.replace(/\s+/g, ' ')
|
|
43
|
+
.trim();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* True when the query, read loosely, is the candidate's name, another name of
|
|
47
|
+
* it, its title, or the last part of its route (`use-a-theme` typed as
|
|
48
|
+
* `use a theme`). Such a query names that entry too, though the lexical
|
|
49
|
+
* ranker's own gate (namesWhole) needs the name exactly.
|
|
50
|
+
* @param {import('./_lexical.mjs').Candidate} candidate
|
|
51
|
+
* @param {string} loose the query, read loosely
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function namesLoosely(candidate, loose) {
|
|
55
|
+
if (!loose) return false;
|
|
56
|
+
const names = [
|
|
57
|
+
candidate.name,
|
|
58
|
+
...(candidate.aliases ?? []),
|
|
59
|
+
candidate._title,
|
|
60
|
+
candidate._topic,
|
|
61
|
+
candidate._resultName,
|
|
62
|
+
candidate._commandName,
|
|
63
|
+
];
|
|
64
|
+
return names.some(name => {
|
|
65
|
+
if (!name) return false;
|
|
66
|
+
const whole = looseName(String(name));
|
|
67
|
+
const last = looseName(String(name).split('/').pop() ?? '');
|
|
68
|
+
return whole === loose || last === loose;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Ranks with one searcher's weights. Shared by the main searcher and every
|
|
74
|
+
* topic searcher, which differ only in their weights.
|
|
75
|
+
* @param {import('./tree.mjs').SearchRequest} request
|
|
76
|
+
* @param {import('./tree.mjs').SearchCatalog} catalog
|
|
77
|
+
* @param {SearcherWeights} weights
|
|
78
|
+
* @returns {import('./search.type.mjs').SearchResponse}
|
|
79
|
+
*/
|
|
80
|
+
export function rankWith(request, catalog, weights) {
|
|
81
|
+
const {query, options} = request;
|
|
82
|
+
// A query that is an entry's whole name gets the lexical answer as is: that
|
|
83
|
+
// entry first, with no near siblings added beside it.
|
|
84
|
+
const term = String(query).trim().toLowerCase();
|
|
85
|
+
const loose = looseName(term);
|
|
86
|
+
if (catalog.all.some(c => namesWhole(c, term) || namesLoosely(c, loose))) {
|
|
87
|
+
return rankLexical(query, options, catalog);
|
|
88
|
+
}
|
|
89
|
+
const table = entryTable(catalog.all, catalog.data.cards);
|
|
90
|
+
const lexical = rankLexical(
|
|
91
|
+
query,
|
|
92
|
+
{...options, limit: Number.MAX_SAFE_INTEGER},
|
|
93
|
+
catalog,
|
|
94
|
+
);
|
|
95
|
+
const route = routeProbs(catalog.data.router, query);
|
|
96
|
+
const rows = signals(table, query, lexical.data.results, route);
|
|
97
|
+
return orderMatches(
|
|
98
|
+
table,
|
|
99
|
+
rank(table, rows, weights),
|
|
100
|
+
lexical,
|
|
101
|
+
query,
|
|
102
|
+
options.limit ?? 20,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @type {import('./tree.mjs').SearchKit} */
|
|
107
|
+
const {defineSearcher} = routeKit;
|
|
108
|
+
|
|
109
|
+
export const main = defineSearcher({
|
|
110
|
+
id: 'main',
|
|
111
|
+
summary:
|
|
112
|
+
'Ranks every component, hook, doc, template, and theme by a weighted sum of plain word-match signals.',
|
|
113
|
+
run: (request, catalog, weights) =>
|
|
114
|
+
rankWith(request, catalog, asWeights(weights) ?? catalog.data.main),
|
|
115
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Tests for the main searcher (_main-searcher.mjs) through search():
|
|
5
|
+
* it ranks by plain signals, answers the same every time, never grows the
|
|
6
|
+
* response past the lexical ranker's, and touches no network. Run against the
|
|
7
|
+
* real @astryxdesign/core.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, it, expect, vi, afterEach} from 'vitest';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import http from 'node:http';
|
|
13
|
+
import https from 'node:https';
|
|
14
|
+
import net from 'node:net';
|
|
15
|
+
import {fileURLToPath} from 'node:url';
|
|
16
|
+
import {search, searchCore} from './search.mjs';
|
|
17
|
+
|
|
18
|
+
const REPO = path.resolve(
|
|
19
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
20
|
+
'../../../..',
|
|
21
|
+
);
|
|
22
|
+
const QUERIES = [
|
|
23
|
+
'date chooser',
|
|
24
|
+
'show a confirmation before deleting',
|
|
25
|
+
'toast notice for a saved draft',
|
|
26
|
+
'grid of rows you can sort by column',
|
|
27
|
+
'how do I install a theme',
|
|
28
|
+
'page of settings with a side menu',
|
|
29
|
+
'Bookmarkz',
|
|
30
|
+
'qzxv',
|
|
31
|
+
];
|
|
32
|
+
/** @param {unknown} r */
|
|
33
|
+
const top = r =>
|
|
34
|
+
/** @type {any} */ (r).data.results.map(x => `${x.domain}:${x.name}`);
|
|
35
|
+
|
|
36
|
+
afterEach(() => vi.restoreAllMocks());
|
|
37
|
+
|
|
38
|
+
describe('main searcher', () => {
|
|
39
|
+
it('finds a component from what it does', async () => {
|
|
40
|
+
expect(
|
|
41
|
+
top(await search('show a confirmation before deleting', {cwd: REPO}))[0],
|
|
42
|
+
).toBe('component:AlertDialog');
|
|
43
|
+
expect(top(await search('date chooser', {cwd: REPO}))[0]).toBe(
|
|
44
|
+
'component:DateInput',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('answers a whole-name query exactly as the lexical ranker does', async () => {
|
|
49
|
+
for (const q of ['Button', 'Dialog', 'useTheme', 'tokens']) {
|
|
50
|
+
expect(top(await search(q, {cwd: REPO}))).toEqual(
|
|
51
|
+
top(await searchCore(q, {cwd: REPO})),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('answers a name typed with spaces for its dashes as the lexical ranker does', async () => {
|
|
57
|
+
for (const q of ['author a theme', 'tokens data']) {
|
|
58
|
+
expect(top(await search(q, {cwd: REPO}))).toEqual(
|
|
59
|
+
top(await searchCore(q, {cwd: REPO})),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('answers the same every time', async () => {
|
|
65
|
+
for (const q of QUERIES.slice(0, 3)) {
|
|
66
|
+
expect(top(await search(q, {cwd: REPO}))).toEqual(
|
|
67
|
+
top(await search(q, {cwd: REPO})),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it(
|
|
73
|
+
'never returns more results or more bytes than the lexical ranker',
|
|
74
|
+
{timeout: 120_000},
|
|
75
|
+
async () => {
|
|
76
|
+
let mine = 0;
|
|
77
|
+
let core = 0;
|
|
78
|
+
for (const q of QUERIES) {
|
|
79
|
+
const [a, b] = [
|
|
80
|
+
await search(q, {cwd: REPO}),
|
|
81
|
+
await searchCore(q, {cwd: REPO}),
|
|
82
|
+
];
|
|
83
|
+
expect(a.data.results.length).toBeLessThanOrEqual(
|
|
84
|
+
b.data.results.length,
|
|
85
|
+
);
|
|
86
|
+
mine += JSON.stringify(a).length;
|
|
87
|
+
core += JSON.stringify(b).length;
|
|
88
|
+
}
|
|
89
|
+
expect(mine).toBeLessThanOrEqual(core);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
it('touches no network', async () => {
|
|
94
|
+
const fail = () => {
|
|
95
|
+
throw new Error('search reached the network');
|
|
96
|
+
};
|
|
97
|
+
vi.spyOn(http, 'request').mockImplementation(fail);
|
|
98
|
+
vi.spyOn(https, 'request').mockImplementation(fail);
|
|
99
|
+
vi.spyOn(net, 'connect').mockImplementation(fail);
|
|
100
|
+
vi.spyOn(net.Socket.prototype, 'connect').mockImplementation(fail);
|
|
101
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(fail);
|
|
102
|
+
expect(
|
|
103
|
+
top(await search('toast notice for a saved draft', {cwd: REPO}))[0],
|
|
104
|
+
).toBe('component:Toast');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"w":[0.1414,-0.004648,0.300865,0.056056,0.392736,0.10546,-0.147371,-0.284355,0.209928,0.04428,0.047343,0.064332,0.189238,-0.023495,-0.108461,0.121128,0.0,0.004331,0.0008,0.088292,0.072215,0.1392,0.24194,0.008913,-0.000386,0.006744,-0.003374,-0.001714,-0.001373,0.046662,0.011763,0.051916,0.155163,0.00274,-0.018366,-0.017329,0.166314,0.186888,0.021662,0.066943,0.046315,0.0,0.0,0.0,0.222553,0.09503,0.121672,0.164462,-0.00524,0.050195,0.059338,0.325721,0.112113,0.05238,0.010799,0.002423,0.003703,0.000179,0.00274],"mu":[0.130015,0.004813,0.187932,0.01647,0.147234,0.050213,0.178723,0.617872,0.005957,0.150607,0.600003,0.113058,0.050851,0.000815,0.06067,0.179093,0.074597,0.001104,1.1e-05,0.016463,0.00663,0.038722,0.068087,0.000114,0.001394,0.000461,0.001084,0.001865,8e-06,0.021395,0.005404,0.028145,0.095511,0.000152,0.125196,0.036065,0.147049,0.290649,0.001044,0.087703,0.025355,0.0,0.0,0.0,0.014431,0.003255,0.013062,0.020055,4.8e-05,0.078997,0.004913,0.028451,0.066588,0.000143,0.000763,3e-05,0.000194,0.000102,1.5e-05],"sd":[0.268781,0.036771,0.372756,0.127248,0.35434,0.218385,0.383122,0.485909,0.076955,0.803588,1.602279,0.680163,0.11702,0.02842,0.22979,0.22327,1e-06,0.033186,0.003276,0.107253,0.069348,0.158982,0.208221,0.008485,0.025221,0.012654,0.015171,0.018277,0.002347,0.32287,0.162291,0.335994,0.644886,0.039907,0.762585,0.446869,0.840132,1.16098,0.094205,0.600728,0.32812,1e-06,1e-06,1e-06,0.071304,0.032858,0.067147,0.071859,0.003777,0.214543,0.045987,0.102079,0.082198,0.00474,0.027608,0.005436,0.013914,0.010108,0.003818]}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/** @typedef {import('./_gather-components.mjs').Candidate} Candidate */
|
|
5
|
+
/**
|
|
6
|
+
* A component or hook doc, compiled, or null when it cannot be read.
|
|
7
|
+
* @param {string} docPath
|
|
8
|
+
* @param {string} [exportName]
|
|
9
|
+
* @param {'components' | 'hooks'} [root]
|
|
10
|
+
* @returns {Promise<any>}
|
|
11
|
+
*/
|
|
12
|
+
export function loadModuleDoc(docPath: string, exportName?: string, root?: "components" | "hooks"): Promise<any>;
|
|
13
|
+
/**
|
|
14
|
+
* Build component candidates from core's own tree: name + keywords +
|
|
15
|
+
* usage/description from the component's .doc.mjs.
|
|
16
|
+
* @param {string} coreDir
|
|
17
|
+
* @returns {Promise<Candidate[]>}
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Usage guidance from a component doc: its feature list and its best-practice
|
|
21
|
+
* advice, flattened to plain strings.
|
|
22
|
+
*
|
|
23
|
+
* This is where a reader's vocabulary usually lives. `Banner` calls itself "a
|
|
24
|
+
* persistent message" and only its guidance names "form errors, system
|
|
25
|
+
* updates, maintenance notices" — so a search for the words people actually
|
|
26
|
+
* type finds nothing without it.
|
|
27
|
+
*
|
|
28
|
+
* @param {any} doc
|
|
29
|
+
* @returns {string[]}
|
|
30
|
+
*/
|
|
31
|
+
export function guidanceFrom(doc: any): string[];
|
|
32
|
+
export type Candidate = import("./_gather-components.mjs").Candidate;
|