@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,78 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Reading a component or hook doc for search: the compiled doc and its
|
|
5
|
+
* usage guidance. Imported only by the adapter's gatherers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {readDocView} from '../../foundation/doc-compiler/read.mjs';
|
|
9
|
+
|
|
10
|
+
/** @typedef {import('./_gather-components.mjs').Candidate} Candidate */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A component or hook doc, compiled, or null when it cannot be read.
|
|
14
|
+
* @param {string} docPath
|
|
15
|
+
* @param {string} [exportName]
|
|
16
|
+
* @param {'components' | 'hooks'} [root]
|
|
17
|
+
* @returns {Promise<any>}
|
|
18
|
+
*/
|
|
19
|
+
export async function loadModuleDoc(
|
|
20
|
+
docPath,
|
|
21
|
+
exportName = 'docs',
|
|
22
|
+
root = 'components',
|
|
23
|
+
) {
|
|
24
|
+
try {
|
|
25
|
+
// Support both the stamped default export and the legacy named export.
|
|
26
|
+
return (
|
|
27
|
+
(await readDocView(docPath, {
|
|
28
|
+
root,
|
|
29
|
+
loader: 'native',
|
|
30
|
+
exports: ['default', exportName],
|
|
31
|
+
})) ?? null
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build component candidates from core's own tree: name + keywords +
|
|
40
|
+
* usage/description from the component's .doc.mjs.
|
|
41
|
+
* @param {string} coreDir
|
|
42
|
+
* @returns {Promise<Candidate[]>}
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Usage guidance from a component doc: its feature list and its best-practice
|
|
47
|
+
* advice, flattened to plain strings.
|
|
48
|
+
*
|
|
49
|
+
* This is where a reader's vocabulary usually lives. `Banner` calls itself "a
|
|
50
|
+
* persistent message" and only its guidance names "form errors, system
|
|
51
|
+
* updates, maintenance notices" — so a search for the words people actually
|
|
52
|
+
* type finds nothing without it.
|
|
53
|
+
*
|
|
54
|
+
* @param {any} doc
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
export function guidanceFrom(doc) {
|
|
58
|
+
if (!doc) return [];
|
|
59
|
+
const features = Array.isArray(doc.features) ? doc.features : [];
|
|
60
|
+
const practices = Array.isArray(doc.usage?.bestPractices)
|
|
61
|
+
? doc.usage.bestPractices
|
|
62
|
+
: [];
|
|
63
|
+
return [...features, ...practices]
|
|
64
|
+
.map(entry =>
|
|
65
|
+
typeof entry === 'string'
|
|
66
|
+
? entry
|
|
67
|
+
: [
|
|
68
|
+
entry?.title,
|
|
69
|
+
entry?.text,
|
|
70
|
+
entry?.description,
|
|
71
|
+
entry?.do,
|
|
72
|
+
entry?.dont,
|
|
73
|
+
]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join(' '),
|
|
76
|
+
)
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
}
|
|
@@ -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
|
+
/**
|
|
5
|
+
* The search response: the lexical matches, each band in the learned order.
|
|
6
|
+
* Nothing is added or dropped, so the count and the scores stay the lexical
|
|
7
|
+
* ranker's; only the order inside a band, and so which matches fill the
|
|
8
|
+
* limit, changes.
|
|
9
|
+
* @param {EntryTable} table
|
|
10
|
+
* @param {number[]} order entry positions, best first, by the searcher's weights
|
|
11
|
+
* @param {import('./search.type.mjs').SearchResponse} lexical every lexical match
|
|
12
|
+
* @param {string} query
|
|
13
|
+
* @param {number} limit
|
|
14
|
+
* @returns {import('./search.type.mjs').SearchResponse}
|
|
15
|
+
*/
|
|
16
|
+
export function orderMatches(table: EntryTable, order: number[], lexical: import("./search.type.mjs").SearchResponse, query: string, limit: number): import("./search.type.mjs").SearchResponse;
|
|
17
|
+
/**
|
|
18
|
+
* An integration's matches that the lexical ranker scored the same keep its
|
|
19
|
+
* order among themselves: they have no cards, so the learned order would only
|
|
20
|
+
* guess between them.
|
|
21
|
+
* @param {Item[]} items
|
|
22
|
+
* @returns {Item[]}
|
|
23
|
+
*/
|
|
24
|
+
export function integrationTies(items: Item[]): Item[];
|
|
25
|
+
export function bandOf(score: number): number;
|
|
26
|
+
export type Item = {
|
|
27
|
+
res: any;
|
|
28
|
+
i: number;
|
|
29
|
+
band: number;
|
|
30
|
+
learned: number;
|
|
31
|
+
};
|
|
32
|
+
export type EntryTable = import("./_entries.mjs").EntryTable;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file How a searcher orders the lexical ranker's matches. Search answers
|
|
5
|
+
* only with what the lexical ranker matched, each with its own score and
|
|
6
|
+
* reason. A match never moves past one of another score band (BANDS below)
|
|
7
|
+
* or another kind, doc or not; between runs the lexical order holds. A band
|
|
8
|
+
* can be wide: 55–99 holds keyword matches beside one-word description hits,
|
|
9
|
+
* and a synonym name match can score under 55, so inside a band a weaker match
|
|
10
|
+
* can lead a stronger one when the weights favor it. Inside such a
|
|
11
|
+
* run the learned order places the matches, with these exceptions: matches
|
|
12
|
+
* that scored the same keep their lexical order, as one block; an entry the
|
|
13
|
+
* query names leads; in a query of one or two words, the matches on every
|
|
14
|
+
* word keep their lexical order; and an integration's matches that scored the
|
|
15
|
+
* same keep theirs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {entryOf} from './_entries.mjs';
|
|
19
|
+
import {words} from './_query-words.mjs';
|
|
20
|
+
import {searchedComponentsOf} from './_result.mjs';
|
|
21
|
+
|
|
22
|
+
/** @typedef {import('./_entries.mjs').EntryTable} EntryTable */
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Where each lexical band starts, highest first: the whole query as a name,
|
|
26
|
+
* as a keyword, as a phrase in a title, a title in the query, every word
|
|
27
|
+
* matched, a strong partial match, a name or keyword match, and below that,
|
|
28
|
+
* prose alone.
|
|
29
|
+
*/
|
|
30
|
+
const BANDS = [195, 188, 170, 160, 151, 100, 55];
|
|
31
|
+
/** The lowest score of the every-word top tier. */
|
|
32
|
+
const FULL_COVERAGE = 151;
|
|
33
|
+
|
|
34
|
+
/** @param {number} score */
|
|
35
|
+
export const bandOf = score => {
|
|
36
|
+
const i = BANDS.findIndex(floor => score >= floor);
|
|
37
|
+
return i === -1 ? BANDS.length : i;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The search response: the lexical matches, each band in the learned order.
|
|
42
|
+
* Nothing is added or dropped, so the count and the scores stay the lexical
|
|
43
|
+
* ranker's; only the order inside a band, and so which matches fill the
|
|
44
|
+
* limit, changes.
|
|
45
|
+
* @param {EntryTable} table
|
|
46
|
+
* @param {number[]} order entry positions, best first, by the searcher's weights
|
|
47
|
+
* @param {import('./search.type.mjs').SearchResponse} lexical every lexical match
|
|
48
|
+
* @param {string} query
|
|
49
|
+
* @param {number} limit
|
|
50
|
+
* @returns {import('./search.type.mjs').SearchResponse}
|
|
51
|
+
*/
|
|
52
|
+
export function orderMatches(table, order, lexical, query, limit) {
|
|
53
|
+
const all = lexical.data.results;
|
|
54
|
+
/** @type {Map<number, number>} */
|
|
55
|
+
const place = new Map(order.map((k, i) => [k, i]));
|
|
56
|
+
// Runs of consecutive lexical matches in one band and of one kind (doc or
|
|
57
|
+
// not) are the only places the learned order acts: a match never crosses
|
|
58
|
+
// into another band, or past a doc or a non-doc the lexical ranker placed.
|
|
59
|
+
let run = -1;
|
|
60
|
+
/** @type {string} */
|
|
61
|
+
let last = '';
|
|
62
|
+
const items = all.map((res, i) => {
|
|
63
|
+
// Each match's own band: the lexical merge can place a weaker match
|
|
64
|
+
// ahead of a stronger doc, and the two must never share a run.
|
|
65
|
+
const key = `${bandOf(res.score ?? 0)}:${res.domain === 'doc'}`;
|
|
66
|
+
if (key !== last) {
|
|
67
|
+
run++;
|
|
68
|
+
last = key;
|
|
69
|
+
}
|
|
70
|
+
const k = entryOf(table, res);
|
|
71
|
+
return {
|
|
72
|
+
res,
|
|
73
|
+
i,
|
|
74
|
+
band: run,
|
|
75
|
+
learned: k === undefined ? Infinity : (place.get(k) ?? Infinity),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
const named = (/** @type {Item} */ it) =>
|
|
79
|
+
(it.res.score ?? 0) >= 95 &&
|
|
80
|
+
/^(exact name|plural of the name|name )/.test(it.res.reason ?? '');
|
|
81
|
+
// Matches the lexical ranker scored the same move as one block, in its
|
|
82
|
+
// order; the learned position of the block's first match places the block
|
|
83
|
+
// in its run (not its best member's, which would let one strong member lift
|
|
84
|
+
// a whole block of weak ones), and an entry the query names leads its run.
|
|
85
|
+
// Moving each match on its own breaks search's existing rules for tied
|
|
86
|
+
// matches (`file required` leads with Field, `ci` with the integration
|
|
87
|
+
// check) and grows answers, so ties move together; where the block lands is
|
|
88
|
+
// measured as spec:AST-083 FR6 requires.
|
|
89
|
+
/** @type {Map<string, number>} */
|
|
90
|
+
const block = new Map();
|
|
91
|
+
const blockOf = (/** @type {Item} */ it) => `${it.band}:${it.res.score}`;
|
|
92
|
+
for (const it of items) {
|
|
93
|
+
const at = named(it) ? -1 : it.learned;
|
|
94
|
+
const k = blockOf(it);
|
|
95
|
+
if (!block.has(k) || at === -1) block.set(k, at);
|
|
96
|
+
}
|
|
97
|
+
let ranked = [...items].sort(
|
|
98
|
+
(x, y) =>
|
|
99
|
+
x.band - y.band ||
|
|
100
|
+
(block.get(blockOf(x)) ?? 0) - (block.get(blockOf(y)) ?? 0) ||
|
|
101
|
+
x.i - y.i,
|
|
102
|
+
);
|
|
103
|
+
if (words(query).length <= 2) {
|
|
104
|
+
ranked = keepLexical(ranked, it => (it.res.score ?? 0) >= FULL_COVERAGE);
|
|
105
|
+
}
|
|
106
|
+
ranked = integrationTies(ranked);
|
|
107
|
+
const response = {
|
|
108
|
+
...lexical,
|
|
109
|
+
data: {...lexical.data, results: ranked.slice(0, limit).map(it => it.res)},
|
|
110
|
+
};
|
|
111
|
+
const searched = searchedComponentsOf.get(lexical);
|
|
112
|
+
if (searched) searchedComponentsOf.set(response, searched);
|
|
113
|
+
return /** @type {import('./search.type.mjs').SearchResponse} */ (response);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @typedef {{res: any, i: number, band: number, learned: number}} Item
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The matches `pick` selects keep the places they hold, filled in lexical
|
|
122
|
+
* order.
|
|
123
|
+
* @param {Item[]} items
|
|
124
|
+
* @param {(item: Item) => boolean} pick
|
|
125
|
+
* @returns {Item[]}
|
|
126
|
+
*/
|
|
127
|
+
function keepLexical(items, pick) {
|
|
128
|
+
/** @type {number[]} */
|
|
129
|
+
const at = [];
|
|
130
|
+
items.forEach((it, j) => pick(it) && at.push(j));
|
|
131
|
+
if (at.length < 2) return items;
|
|
132
|
+
const sorted = at.map(j => items[j]).sort((a, b) => a.i - b.i);
|
|
133
|
+
const out = [...items];
|
|
134
|
+
at.forEach((j, n) => (out[j] = sorted[n]));
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An integration's matches that the lexical ranker scored the same keep its
|
|
140
|
+
* order among themselves: they have no cards, so the learned order would only
|
|
141
|
+
* guess between them.
|
|
142
|
+
* @param {Item[]} items
|
|
143
|
+
* @returns {Item[]}
|
|
144
|
+
*/
|
|
145
|
+
export function integrationTies(items) {
|
|
146
|
+
/** @type {Map<number, number[]>} */
|
|
147
|
+
const groups = new Map();
|
|
148
|
+
items.forEach((it, j) => {
|
|
149
|
+
const pkg = it.res.package ?? '';
|
|
150
|
+
if (!pkg || pkg.startsWith('@astryxdesign/')) return;
|
|
151
|
+
const key = it.res.score ?? 0;
|
|
152
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
153
|
+
/** @type {number[]} */ (groups.get(key)).push(j);
|
|
154
|
+
});
|
|
155
|
+
const out = [...items];
|
|
156
|
+
for (const at of groups.values()) {
|
|
157
|
+
if (at.length < 2) continue;
|
|
158
|
+
const sorted = at.map(j => items[j]).sort((a, b) => a.i - b.i);
|
|
159
|
+
at.forEach((j, n) => (out[j] = sorted[n]));
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Invariants of search's ordering, checked against the lexical ranker on
|
|
5
|
+
* the real catalog: search answers only with lexical matches, keeps each one's
|
|
6
|
+
* score and reason, and never moves a match past one of another band or kind.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {describe, it, expect} from 'vitest';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import {fileURLToPath} from 'node:url';
|
|
12
|
+
import {search, searchCore} from './search.mjs';
|
|
13
|
+
import {bandOf} from './_order.mjs';
|
|
14
|
+
|
|
15
|
+
const cwd = path.resolve(
|
|
16
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
+
'../../../..',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/** @param {any} r */
|
|
21
|
+
const keyOf = r => `${r.domain}:${r.name}:${r.command}`;
|
|
22
|
+
/** @param {any} r */
|
|
23
|
+
const runOf = r => `${bandOf(r.score ?? 0)}:${r.domain === 'doc'}`;
|
|
24
|
+
|
|
25
|
+
const QUERIES = [
|
|
26
|
+
'account settings',
|
|
27
|
+
'table of contents',
|
|
28
|
+
'content',
|
|
29
|
+
'dark mode',
|
|
30
|
+
'illustration',
|
|
31
|
+
'date picker in a card',
|
|
32
|
+
'how do I add a theme',
|
|
33
|
+
'button',
|
|
34
|
+
'side panel that resizes',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
describe('search ordering invariants', () => {
|
|
38
|
+
for (const q of QUERIES) {
|
|
39
|
+
it(`keeps the lexical matches, bands and kinds for "${q}"`, async () => {
|
|
40
|
+
const ours = await search(q, {cwd, limit: 50});
|
|
41
|
+
const lexical = await searchCore(q, {
|
|
42
|
+
cwd,
|
|
43
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
44
|
+
});
|
|
45
|
+
expect(ours.data.matchCount).toBe(lexical.data.matchCount);
|
|
46
|
+
expect(ours.data.results.length).toBeLessThanOrEqual(
|
|
47
|
+
Math.min(50, lexical.data.matchCount),
|
|
48
|
+
);
|
|
49
|
+
/** @type {Map<string, number>} */
|
|
50
|
+
const at = new Map(lexical.data.results.map((r, i) => [keyOf(r), i]));
|
|
51
|
+
const byKey = new Map(lexical.data.results.map(r => [keyOf(r), r]));
|
|
52
|
+
for (const r of ours.data.results) {
|
|
53
|
+
const own = byKey.get(keyOf(r));
|
|
54
|
+
expect(own, `${keyOf(r)} is a lexical match`).toBeDefined();
|
|
55
|
+
expect(r.score).toBe(own?.score);
|
|
56
|
+
expect(r.reason).toBe(own?.reason);
|
|
57
|
+
}
|
|
58
|
+
// Two results of different runs (band or kind) keep the lexical order.
|
|
59
|
+
const rs = ours.data.results;
|
|
60
|
+
for (let i = 0; i < rs.length; i++) {
|
|
61
|
+
for (let j = i + 1; j < rs.length; j++) {
|
|
62
|
+
if (runOf(rs[i]) === runOf(rs[j])) continue;
|
|
63
|
+
expect(
|
|
64
|
+
/** @type {number} */ (at.get(keyOf(rs[i]))) <
|
|
65
|
+
/** @type {number} */ (at.get(keyOf(rs[j]))),
|
|
66
|
+
`${keyOf(rs[i])} above ${keyOf(rs[j])}`,
|
|
67
|
+
).toBe(true);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}, 60_000);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
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('./_lexical.mjs').Candidate} Candidate */
|
|
5
|
+
/**
|
|
6
|
+
* A match's domain priority: 0 (none), 1, or 2 (a doc the whole query names).
|
|
7
|
+
* A component, hook, template, or theme with priority ranks ahead of any doc
|
|
8
|
+
* without it, whatever the two scores are. Docs among themselves rank by
|
|
9
|
+
* priority, then score; everything else ranks among itself by score.
|
|
10
|
+
*
|
|
11
|
+
* A component, hook, template, or theme has priority (1) when one of the
|
|
12
|
+
* query's words, or the whole query, hits its name or an authored keyword at
|
|
13
|
+
* {@link STRONG_TOKEN_SCORE} or above, or when it matches every word.
|
|
14
|
+
*
|
|
15
|
+
* A doc has priority when the reader asked for that doc:
|
|
16
|
+
* - 2: the whole query is the topic's name, its last route segment, or its
|
|
17
|
+
* own title, as words and whatever the plural (`font setup` is
|
|
18
|
+
* typography/font-setup, `side panel` is "Side panels");
|
|
19
|
+
* - 1: a word of the query is the topic's name (`illustration` in a longer
|
|
20
|
+
* question is the illustrations guide);
|
|
21
|
+
* - 1: a query of two or more words is one of the topic's own declared
|
|
22
|
+
* keywords, whatever the plural (`code review` on a guide that declares it);
|
|
23
|
+
* - 1: the query holds one of the doc's titles whole (`resizable side panels`
|
|
24
|
+
* names "Side panels"; `light dark mode button` names "Light/Dark Mode");
|
|
25
|
+
* - 1: the doc matches every word of the query and one of them is a word of
|
|
26
|
+
* its title (`switch to a dark theme` and "Use a theme").
|
|
27
|
+
* A section answers to its topic's name.
|
|
28
|
+
*
|
|
29
|
+
* A doc that matched only by a heading holding the query, by one word, or by
|
|
30
|
+
* words spread through its text has no priority. Docs are split into many
|
|
31
|
+
* small topics, each with its own headings, so a common phrase such as
|
|
32
|
+
* `font size` hits a heading exactly (170) while the component the reader is
|
|
33
|
+
* after matches one word by keyword (`Text`, 98). Ranked on text alone, every
|
|
34
|
+
* split adds another doc above the component. Broad reference pages match every word of many queries
|
|
35
|
+
* in their text the same way. The score stays the text-match strength the
|
|
36
|
+
* result reports, so callers that gate on it (`build`) see the same numbers.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} term - Lowercased search term.
|
|
39
|
+
* @param {string[]} tokens - Content tokens from tokenizeQuery(term).
|
|
40
|
+
* @param {Candidate} candidate
|
|
41
|
+
* @param {{score: number, matched: number, total: number}} hit - The
|
|
42
|
+
* candidate's scoreQuery result.
|
|
43
|
+
* @returns {0 | 1 | 2}
|
|
44
|
+
*/
|
|
45
|
+
export function domainPriority(term: string, tokens: string[], candidate: Candidate, hit: {
|
|
46
|
+
score: number;
|
|
47
|
+
matched: number;
|
|
48
|
+
total: number;
|
|
49
|
+
}, queryWords?: string[]): 0 | 1 | 2;
|
|
50
|
+
export type Candidate = import("./_lexical.mjs").Candidate;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Domain priority: when a component, hook, template, or theme ranks
|
|
5
|
+
* ahead of docs the reader did not ask for.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {sameWord} from './_words.mjs';
|
|
9
|
+
import {phraseWords, samePhraseWord, samePhrase} from './_query.mjs';
|
|
10
|
+
import {compoundHit, compoundOf} from './_compound.mjs';
|
|
11
|
+
import {
|
|
12
|
+
bestForToken,
|
|
13
|
+
FULL_COVERAGE_SCORE,
|
|
14
|
+
STRONG_TOKEN_SCORE,
|
|
15
|
+
} from './_score.mjs';
|
|
16
|
+
import {TITLE_PHRASE_SCORE, TITLE_IN_QUERY_SCORE} from './_titles.mjs';
|
|
17
|
+
import {scoreCandidate} from './_candidate-score.mjs';
|
|
18
|
+
|
|
19
|
+
/** @typedef {import('./_lexical.mjs').Candidate} Candidate */
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A match's domain priority: 0 (none), 1, or 2 (a doc the whole query names).
|
|
23
|
+
* A component, hook, template, or theme with priority ranks ahead of any doc
|
|
24
|
+
* without it, whatever the two scores are. Docs among themselves rank by
|
|
25
|
+
* priority, then score; everything else ranks among itself by score.
|
|
26
|
+
*
|
|
27
|
+
* A component, hook, template, or theme has priority (1) when one of the
|
|
28
|
+
* query's words, or the whole query, hits its name or an authored keyword at
|
|
29
|
+
* {@link STRONG_TOKEN_SCORE} or above, or when it matches every word.
|
|
30
|
+
*
|
|
31
|
+
* A doc has priority when the reader asked for that doc:
|
|
32
|
+
* - 2: the whole query is the topic's name, its last route segment, or its
|
|
33
|
+
* own title, as words and whatever the plural (`font setup` is
|
|
34
|
+
* typography/font-setup, `side panel` is "Side panels");
|
|
35
|
+
* - 1: a word of the query is the topic's name (`illustration` in a longer
|
|
36
|
+
* question is the illustrations guide);
|
|
37
|
+
* - 1: a query of two or more words is one of the topic's own declared
|
|
38
|
+
* keywords, whatever the plural (`code review` on a guide that declares it);
|
|
39
|
+
* - 1: the query holds one of the doc's titles whole (`resizable side panels`
|
|
40
|
+
* names "Side panels"; `light dark mode button` names "Light/Dark Mode");
|
|
41
|
+
* - 1: the doc matches every word of the query and one of them is a word of
|
|
42
|
+
* its title (`switch to a dark theme` and "Use a theme").
|
|
43
|
+
* A section answers to its topic's name.
|
|
44
|
+
*
|
|
45
|
+
* A doc that matched only by a heading holding the query, by one word, or by
|
|
46
|
+
* words spread through its text has no priority. Docs are split into many
|
|
47
|
+
* small topics, each with its own headings, so a common phrase such as
|
|
48
|
+
* `font size` hits a heading exactly (170) while the component the reader is
|
|
49
|
+
* after matches one word by keyword (`Text`, 98). Ranked on text alone, every
|
|
50
|
+
* split adds another doc above the component. Broad reference pages match every word of many queries
|
|
51
|
+
* in their text the same way. The score stays the text-match strength the
|
|
52
|
+
* result reports, so callers that gate on it (`build`) see the same numbers.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} term - Lowercased search term.
|
|
55
|
+
* @param {string[]} tokens - Content tokens from tokenizeQuery(term).
|
|
56
|
+
* @param {Candidate} candidate
|
|
57
|
+
* @param {{score: number, matched: number, total: number}} hit - The
|
|
58
|
+
* candidate's scoreQuery result.
|
|
59
|
+
* @returns {0 | 1 | 2}
|
|
60
|
+
*/
|
|
61
|
+
export function domainPriority(
|
|
62
|
+
term,
|
|
63
|
+
tokens,
|
|
64
|
+
candidate,
|
|
65
|
+
hit,
|
|
66
|
+
queryWords = phraseWords(term),
|
|
67
|
+
) {
|
|
68
|
+
const words = [term, ...tokens];
|
|
69
|
+
if (candidate.domain === 'doc') {
|
|
70
|
+
const topic = String(candidate._topic ?? candidate.name).toLowerCase();
|
|
71
|
+
// The whole query, read as words, is the topic's route, its last
|
|
72
|
+
// segment, or its own title, whatever the plural: `side panel` is
|
|
73
|
+
// layout/side-panels, and `header and footer` is "Headers and footers".
|
|
74
|
+
// A section's title is a heading inside a topic, not the topic's name.
|
|
75
|
+
const named = [topic, topic.slice(topic.lastIndexOf('/') + 1)];
|
|
76
|
+
if (candidate._section == null && candidate.titles?.[0]) {
|
|
77
|
+
named.push(candidate.titles[0]);
|
|
78
|
+
}
|
|
79
|
+
if (named.some(n => samePhrase(term, n))) {
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
// A query of two or more words that is one of the doc's own declared
|
|
83
|
+
// keywords, read as words whatever the plural, is the author naming the
|
|
84
|
+
// doc for that phrase: `code review` on a guide that declares it. Only
|
|
85
|
+
// what the author declared counts: the code terms and table cells a
|
|
86
|
+
// doc's other keywords add (`missing_field`) name what it mentions, not
|
|
87
|
+
// what it is for. One word is left out too, and a section declares
|
|
88
|
+
// nothing.
|
|
89
|
+
if (
|
|
90
|
+
phraseWords(term).length >= 2 &&
|
|
91
|
+
candidate._section == null &&
|
|
92
|
+
(candidate._declared ?? []).some(k => samePhrase(term, String(k)))
|
|
93
|
+
) {
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
words.some(
|
|
98
|
+
w =>
|
|
99
|
+
(scoreCandidate(w, {name: topic}, {fuzzy: false})?.score ?? 0) >= 95,
|
|
100
|
+
)
|
|
101
|
+
) {
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
// A whole title inside the query is the only path to 160-169.
|
|
105
|
+
if (hit.score >= TITLE_IN_QUERY_SCORE && hit.score < TITLE_PHRASE_SCORE) {
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
// Every word matched, and one of them names the doc in its title.
|
|
109
|
+
if (
|
|
110
|
+
hit.score >= FULL_COVERAGE_SCORE &&
|
|
111
|
+
hit.score < TITLE_IN_QUERY_SCORE &&
|
|
112
|
+
hit.matched === hit.total
|
|
113
|
+
) {
|
|
114
|
+
const titleWords = phraseWords(candidate.titles?.[0] ?? '');
|
|
115
|
+
if (
|
|
116
|
+
(tokens.length ? tokens : [term]).some(t =>
|
|
117
|
+
titleWords.some(w => samePhraseWord(t, w) || sameWord(t, w)),
|
|
118
|
+
)
|
|
119
|
+
) {
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (hit.score >= FULL_COVERAGE_SCORE) return 1;
|
|
126
|
+
const compound = compoundOf.has(candidate)
|
|
127
|
+
? compoundOf.get(candidate)
|
|
128
|
+
: compoundHit(queryWords, candidate);
|
|
129
|
+
if ((compound?.score ?? 0) >= STRONG_TOKEN_SCORE) {
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
const fuzzy = tokens.length <= 1;
|
|
133
|
+
return words.some(
|
|
134
|
+
w =>
|
|
135
|
+
(bestForToken(w, candidate, {fuzzy})?.score ?? 0) >= STRONG_TOKEN_SCORE,
|
|
136
|
+
)
|
|
137
|
+
? 1
|
|
138
|
+
: 0;
|
|
139
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
/** Plural folding: one fixed rule per suffix. @param {string} w */
|
|
5
|
+
export function fold(w: string): string;
|
|
6
|
+
/** A name's words: `DateRangeInput` is date, range, input. @param {string} name */
|
|
7
|
+
export function nameWords(name: string): string[];
|
|
8
|
+
/** @param {string} q @returns {string[]} unique words, then adjacent pairs */
|
|
9
|
+
export function queryTokens(q: string): string[];
|
|
10
|
+
/** The router's inputs: the query's tokens plus four shape flags. @param {string} q */
|
|
11
|
+
export function routerTokens(q: string): string[];
|
|
12
|
+
/**
|
|
13
|
+
* The router's probability for each class: the shared classifier over the
|
|
14
|
+
* query's tokens.
|
|
15
|
+
* @param {import('../../foundation/route/route.mjs').ClassifierWeights} weights
|
|
16
|
+
* @param {string} q
|
|
17
|
+
* @returns {number[]}
|
|
18
|
+
*/
|
|
19
|
+
export function routeProbs(weights: import("../../foundation/route/route.mjs").ClassifierWeights, q: string): number[];
|
|
20
|
+
/** The domains, in the order the weights files index them. */
|
|
21
|
+
export const DOMAINS: string[];
|
|
22
|
+
/** The router's classes: the domain a query wants, or none. */
|
|
23
|
+
export const ROUTES: string[];
|
|
24
|
+
export function words(text: string): string[];
|
|
25
|
+
export function queryWords(q: string): string[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file How search's searchers and classifier read a query: its words,
|
|
5
|
+
* folded forms, word pairs, and the classifier's tokens and class
|
|
6
|
+
* probabilities.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {classify} from '../../foundation/route/route.mjs';
|
|
10
|
+
|
|
11
|
+
/** The domains, in the order the weights files index them. */
|
|
12
|
+
export const DOMAINS = ['component', 'hook', 'doc', 'template', 'theme'];
|
|
13
|
+
|
|
14
|
+
/** The router's classes: the domain a query wants, or none. */
|
|
15
|
+
export const ROUTES = [...DOMAINS, 'NONE'];
|
|
16
|
+
|
|
17
|
+
const STOP = new Set(
|
|
18
|
+
'a an the of for to with and or in on by is it my me i we you this that be as at from use using how what which show make add create get set any can do does not no into via'.split(
|
|
19
|
+
' ',
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const QUESTION = new Set([
|
|
24
|
+
'how',
|
|
25
|
+
'what',
|
|
26
|
+
'why',
|
|
27
|
+
'when',
|
|
28
|
+
'where',
|
|
29
|
+
'can',
|
|
30
|
+
'does',
|
|
31
|
+
'is',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/** @param {string} text @returns {string[]} */
|
|
35
|
+
export const words = text =>
|
|
36
|
+
(text || '').toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
37
|
+
|
|
38
|
+
/** Plural folding: one fixed rule per suffix. @param {string} w */
|
|
39
|
+
export function fold(w) {
|
|
40
|
+
if (w.length > 4 && w.endsWith('ies')) return `${w.slice(0, -3)}y`;
|
|
41
|
+
if (w.length > 3 && w.endsWith('es') && 'sxz'.includes(w[w.length - 3])) {
|
|
42
|
+
return w.slice(0, -2);
|
|
43
|
+
}
|
|
44
|
+
if (w.length > 3 && w.endsWith('s') && !w.endsWith('ss'))
|
|
45
|
+
return w.slice(0, -1);
|
|
46
|
+
return w;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A name's words: `DateRangeInput` is date, range, input. @param {string} name */
|
|
50
|
+
export function nameWords(name) {
|
|
51
|
+
const s = name
|
|
52
|
+
.replace(/[/-]/g, ' ')
|
|
53
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
54
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
55
|
+
return words(s);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The query's content words, folded. @param {string} q */
|
|
59
|
+
export const queryWords = q =>
|
|
60
|
+
words(q)
|
|
61
|
+
.filter(w => !STOP.has(w))
|
|
62
|
+
.map(fold);
|
|
63
|
+
|
|
64
|
+
/** @param {string} q @returns {string[]} unique words, then adjacent pairs */
|
|
65
|
+
export function queryTokens(q) {
|
|
66
|
+
const qw = queryWords(q);
|
|
67
|
+
const pairs = qw.slice(1).map((w, i) => `${qw[i]}_${w}`);
|
|
68
|
+
return [...new Set([...qw, ...pairs])];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The router's inputs: the query's tokens plus four shape flags. @param {string} q */
|
|
72
|
+
export function routerTokens(q) {
|
|
73
|
+
const ws = words(q);
|
|
74
|
+
const flags = [`#n${Math.min(ws.length, 6)}`];
|
|
75
|
+
if (ws.length && QUESTION.has(ws[0])) flags.push('#question');
|
|
76
|
+
if (q.split(/\s+/).some(w => /^use[a-z]+/.test(w)) || /\buse[A-Z]/.test(q)) {
|
|
77
|
+
flags.push('#usehook');
|
|
78
|
+
}
|
|
79
|
+
if (/[A-Z][a-z]+[A-Z]/.test(q)) flags.push('#camel');
|
|
80
|
+
return [...queryTokens(q), ...flags, '#bias'];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The router's probability for each class: the shared classifier over the
|
|
85
|
+
* query's tokens.
|
|
86
|
+
* @param {import('../../foundation/route/route.mjs').ClassifierWeights} weights
|
|
87
|
+
* @param {string} q
|
|
88
|
+
* @returns {number[]}
|
|
89
|
+
*/
|
|
90
|
+
export function routeProbs(weights, q) {
|
|
91
|
+
return classify(weights, routerTokens(q));
|
|
92
|
+
}
|