@astryxdesign/cli 0.7.2 → 0.7.3-canary.02f5fe4
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 +104 -71
- package/api/build/_adapter.d.mts +58 -0
- package/api/build/_adapter.mjs +127 -3
- package/api/build/blocks/blocks.d.mts +92 -0
- package/api/build/blocks/blocks.mjs +528 -0
- package/api/build/blocks/blocks.test.mjs +238 -0
- package/api/build/blocks/blocks.weights.json +1 -0
- package/api/build/build.mjs +25 -6
- package/api/build/build.type.mjs +1 -1
- package/api/build/kit/answer.d.mts +18 -0
- package/api/build/kit/answer.mjs +195 -0
- package/api/build/kit/answer.test.mjs +108 -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 +79 -0
- package/api/build/kit/signals.mjs +295 -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 +53 -0
- package/api/build/tree.test.mjs +185 -0
- package/api/build/tree.weights.json +1 -0
- package/api/component/_adapter.mjs +39 -5
- package/api/component/component-replacement.test.mjs +1 -0
- package/api/component/component.mjs +17 -3
- package/api/component/component.test.mjs +15 -0
- package/api/component/doc-links.test.mjs +2 -2
- package/api/discover/_adapter.d.mts +1 -2
- package/api/discover/_adapter.mjs +42 -17
- package/api/discover/_adapter.test.mjs +42 -0
- package/api/discover/discover.mjs +0 -1
- package/api/docs/_adapter.d.mts +2 -2
- package/api/docs/_adapter.mjs +82 -23
- package/api/docs/docs-homes.test.mjs +127 -0
- package/api/docs/docs.doc.mjs +3 -3
- package/api/docs/docs.test.mjs +35 -19
- package/api/docs/integration-tree.test.mjs +20 -20
- package/api/docs/integrationDocs.test.mjs +1 -1
- package/api/docs/namespace-section.test.mjs +6 -6
- package/api/docs/reference-blocks.test.mjs +2 -2
- package/api/doctor/_adapter.d.mts +33 -13
- package/api/doctor/_adapter.mjs +55 -25
- package/api/doctor/doctor.d.mts +23 -5
- package/api/doctor/doctor.mjs +183 -40
- package/api/doctor/doctor.test.mjs +95 -12
- package/api/hook/hook.mjs +1 -1
- 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/_adapter.d.mts +8 -0
- package/api/integration/_adapter.mjs +21 -1
- package/api/integration/authoring-checks.mjs +83 -9
- package/api/integration/authoring-checks.test.mjs +220 -0
- 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-fixes.test.mjs +6 -5
- package/api/integration/validate-integration-gaps.test.mjs +1975 -0
- package/api/integration/validate-integration.d.mts +15 -2
- package/api/integration/validate-integration.mjs +298 -39
- package/api/integration/validate-integration.type.d.mts +90 -1
- package/api/integration/validate-integration.type.mjs +67 -2
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/search/ORDERING.md +28 -0
- package/api/search/_adapter.d.mts +82 -2
- package/api/search/_adapter.mjs +157 -10
- package/api/search/_adapter.test.mjs +173 -0
- 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 +63 -0
- package/api/search/_entries.mjs +232 -0
- package/api/search/_entries.test.mjs +48 -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 +8 -6
- package/api/search/search.mjs +56 -2482
- package/api/search/search.test.mjs +14 -14
- 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/template/template.mjs +5 -1
- 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/author-a-theme.doc.mjs +2 -2
- package/assets/docs/is-my-code-good.doc.mjs +287 -0
- package/assets/docs/tree/add-a-component.doc.mjs +1 -1
- package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
- package/assets/docs/tree/add-a-topic.doc.mjs +2 -2
- package/assets/docs/tree/agent-guidance.doc.mjs +1 -1
- package/assets/docs/tree/api.doc.mjs +6 -0
- package/assets/docs/tree/{integrations.doc.mjs → authoring.doc.mjs} +15 -6
- package/assets/docs/tree/block-template.doc.mjs +1 -1
- package/assets/docs/tree/build-the-template.doc.mjs +1 -1
- package/assets/docs/tree/building-blocks.doc.mjs +2 -2
- package/assets/docs/tree/check-the-props.doc.mjs +1 -1
- package/assets/docs/tree/check-your-docs.doc.mjs +2 -2
- package/assets/docs/tree/checks.doc.mjs +3 -3
- package/assets/docs/tree/ci-and-containers.doc.mjs +13 -0
- package/assets/docs/tree/cli-command.doc.mjs +1 -1
- package/assets/docs/tree/cli.doc.mjs +1 -1
- package/assets/docs/tree/codemods.doc.mjs +1 -1
- package/assets/docs/tree/component-family.doc.mjs +1 -1
- package/assets/docs/tree/component-imports.doc.mjs +1 -1
- package/assets/docs/tree/components.doc.mjs +1 -1
- package/assets/docs/tree/configuration.doc.mjs +1 -1
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
- package/assets/docs/tree/define-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/docs.doc.mjs +1 -1
- package/assets/docs/tree/document-the-template.doc.mjs +1 -1
- package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
- package/assets/docs/tree/extend-or-replace.doc.mjs +1 -1
- package/assets/docs/{authoring.doc.mjs → tree/field-reference.doc.mjs} +4 -3
- package/assets/docs/tree/fonts-and-assets.doc.mjs +1 -1
- package/assets/docs/tree/generate-a-palette.doc.mjs +1 -1
- package/assets/docs/tree/global-integrations.doc.mjs +2 -2
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +4 -4
- package/assets/docs/tree/help.doc.mjs +2 -2
- package/assets/docs/tree/host-entry.doc.mjs +128 -0
- package/assets/docs/tree/links.doc.mjs +1 -1
- package/assets/docs/tree/package-and-test.doc.mjs +1 -1
- package/assets/docs/tree/page-template.doc.mjs +1 -1
- package/assets/docs/tree/publishing.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-component.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
- package/assets/docs/tree/sections-and-placement.doc.mjs +1 -1
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/ship.doc.mjs +2 -2
- package/assets/docs/tree/short-and-findable.doc.mjs +1 -1
- package/assets/docs/tree/single-component.doc.mjs +1 -1
- package/assets/docs/tree/start-a-template.doc.mjs +1 -1
- package/assets/docs/tree/subcomponent.doc.mjs +1 -1
- package/assets/docs/tree/template-assets.doc.mjs +1 -1
- package/assets/docs/tree/template-doc-overview.doc.mjs +2 -2
- package/assets/docs/tree/template-fonts.doc.mjs +1 -1
- package/assets/docs/tree/template-grading-rubric.doc.mjs +17 -38
- package/assets/docs/tree/template-icons.doc.mjs +1 -1
- package/assets/docs/tree/template-images-media.doc.mjs +1 -1
- package/assets/docs/tree/template-styles.doc.mjs +1 -1
- package/assets/docs/tree/templates.doc.mjs +1 -1
- package/assets/docs/tree/test-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
- package/assets/docs/tree/themes.doc.mjs +1 -1
- package/assets/docs/tree/tokens-data.doc.mjs +2 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +1 -1
- package/assets/docs/tree/upgrading.doc.mjs +1 -1
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +1 -1
- package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
- package/assets/docs/tree/versioning.doc.mjs +5 -1
- package/assets/docs/tree/write-good-templates.doc.mjs +1 -1
- package/assets/docs/tree/write-the-template-file.doc.mjs +1 -1
- package/assets/docs/use-integrations.doc.mjs +120 -0
- 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/authoring/doctypes/schema/schema.doc.mjs +1 -1
- package/authoring/integration/integration.doc.mjs +1 -1
- package/authoring/integration/type.ts +1 -1
- package/clients/cli/bin/astryx.mjs +7 -78
- package/clients/cli/command-result-coverage.test.mjs +6 -5
- package/clients/cli/commands/build-theme.color-scheme.test.mjs +1 -1
- package/clients/cli/commands/{component/index.mjs → component.mjs} +37 -37
- package/clients/cli/commands/component.test.mjs +6 -6
- package/clients/cli/commands/docs.doc.mjs +2 -2
- package/clients/cli/commands/docs.mjs +1 -1
- package/clients/cli/commands/docs.test.mjs +14 -14
- package/clients/cli/commands/doctor-integration.test.mjs +49 -1
- package/clients/cli/commands/doctor.doc.mjs +1 -1
- package/clients/cli/commands/doctor.mjs +40 -0
- package/clients/cli/commands/doctor.test.mjs +65 -0
- package/clients/cli/commands/{hook/index.mjs → hook.mjs} +22 -22
- package/clients/cli/commands/import-hint-correctness.test.mjs +1 -1
- 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/integration-check.test.mjs +48 -0
- package/clients/cli/commands/integration.doc.mjs +1 -1
- package/clients/cli/commands/manifest.doc.mjs +1 -1
- package/clients/cli/commands/manifest.mjs +67 -0
- package/clients/cli/commands/outside-project.test.mjs +101 -1
- package/clients/cli/commands/postinstall.doc.mjs +23 -0
- package/clients/cli/commands/postinstall.mjs +31 -0
- package/clients/cli/commands/setup-nudge.test.mjs +78 -0
- package/clients/cli/commands/{build-theme.mjs → theme.mjs} +1 -1
- 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 +54 -94
- package/clients/cli/lib/component-format.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +12 -7
- package/clients/cli/lib/exit-codes.test.mjs +5 -0
- package/clients/cli/lib/manifest.test.mjs +12 -0
- package/clients/cli/lib/welcome-box.mjs +50 -0
- package/clients/cli/run.mjs +108 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +32 -7
- package/foundation/agent-docs/agent-doc-state.mjs +58 -9
- package/foundation/agent-docs/agent-docs.d.mts +100 -12
- package/foundation/agent-docs/agent-docs.mjs +1017 -105
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +1239 -17
- package/foundation/agent-docs/agent-docs.test.mjs +683 -39
- package/foundation/agent-docs/computer-agent-text.d.mts +39 -2
- package/foundation/agent-docs/computer-agent-text.mjs +135 -8
- package/foundation/agent-docs/computer-agent-text.test.mjs +99 -2
- package/foundation/config/project.mjs +20 -22
- package/foundation/config/project.test.mjs +56 -0
- package/foundation/discovery/authoring-self-docs.d.mts +7 -0
- package/foundation/discovery/authoring-self-docs.mjs +16 -4
- package/foundation/discovery/authoring-self-docs.test.mjs +15 -3
- package/foundation/discovery/authoring-surface.d.mts +1 -1
- package/foundation/discovery/authoring-surface.mjs +2 -2
- package/foundation/discovery/authoring-surface.test.mjs +1 -1
- package/foundation/discovery/cli-self-docs.d.mts +2 -1
- package/foundation/discovery/cli-self-docs.mjs +4 -3
- package/foundation/discovery/cli-self-docs.test.mjs +2 -2
- 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/runner-allowlist.d.mts +2 -4
- package/foundation/discovery/runner-allowlist.mjs +2 -4
- package/foundation/discovery/runner-commands.d.mts +77 -0
- package/foundation/discovery/runner-commands.mjs +294 -0
- package/foundation/discovery/runner-commands.test.mjs +128 -0
- package/foundation/discovery/stray-pointers.test.mjs +1 -1
- 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 +33 -9
- package/foundation/doc-compiler/read.mjs +59 -1
- package/foundation/doc-compiler/read.test.mjs +11 -2
- package/foundation/doc-compiler/split-topic-keywords.test.mjs +3 -2
- package/foundation/doc-compiler/tree.d.mts +32 -6
- package/foundation/doc-compiler/tree.mjs +77 -43
- package/foundation/doc-compiler/tree.test.mjs +7 -7
- 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/fs/read-record.d.mts +18 -0
- package/foundation/fs/read-record.mjs +131 -0
- package/foundation/fs/read-record.test.mjs +77 -0
- package/foundation/integrations/autolink.d.mts +2 -1
- package/foundation/integrations/autolink.mjs +4 -13
- package/foundation/integrations/cli-peer-version.d.mts +36 -0
- package/foundation/integrations/cli-peer-version.mjs +132 -0
- package/foundation/integrations/cli-peer-version.test.mjs +354 -0
- 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/exports-subpaths.d.mts +57 -0
- package/foundation/integrations/exports-subpaths.mjs +113 -0
- package/foundation/integrations/integrations.d.mts +27 -1
- package/foundation/integrations/integrations.mjs +61 -2
- package/foundation/integrations/public-imports.d.mts +118 -0
- package/foundation/integrations/public-imports.mjs +1769 -0
- package/foundation/integrations/public-imports.test.mjs +1793 -0
- package/foundation/integrations/validate-contributions.d.mts +115 -0
- package/foundation/integrations/validate-contributions.mjs +1168 -13
- package/foundation/release/cache.test.mjs +24 -21
- package/foundation/release/global-state.d.mts +8 -4
- package/foundation/release/global-state.mjs +4 -2
- package/foundation/release/handoff.d.mts +23 -4
- package/foundation/release/handoff.mjs +175 -8
- package/foundation/release/notices.d.mts +62 -8
- package/foundation/release/notices.mjs +136 -25
- package/foundation/release/notices.test.mjs +21 -0
- 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/foundation/text/string-utils.d.mts +2 -1
- package/foundation/text/string-utils.mjs +16 -5
- package/foundation/text/suggest-order.d.mts +72 -0
- package/foundation/text/suggest-order.mjs +363 -0
- package/foundation/text/suggest-order.test.mjs +259 -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,150 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Search's arguments and results: argument checks, the result each
|
|
5
|
+
* candidate becomes, and the components a search read.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {AstryxError} from '../error.mjs';
|
|
9
|
+
import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
|
|
10
|
+
import {setResultCoverage} from './coverage.mjs';
|
|
11
|
+
import {SEARCH_DOMAINS} from './_query.mjs';
|
|
12
|
+
|
|
13
|
+
/** @typedef {import('./_lexical.mjs').Candidate} Candidate */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Map a scored candidate to its public, actionable result shape. Each result
|
|
17
|
+
* carries enough to act on it: the domain, name, a one-line description, and
|
|
18
|
+
* the follow-up command (and import path where relevant).
|
|
19
|
+
*
|
|
20
|
+
* @param {Candidate} c - candidate
|
|
21
|
+
* @param {number} score
|
|
22
|
+
* @param {string} reason
|
|
23
|
+
* @param {number} matchedTerms - Query concepts this result answered.
|
|
24
|
+
* @param {number} queryTerms - Query concepts there were to answer.
|
|
25
|
+
* @param {boolean} [outside] - The search runs outside a project, where a
|
|
26
|
+
* theme cannot be added to an app, so its command forks the source instead
|
|
27
|
+
* (spec AST-081 FR3).
|
|
28
|
+
*/
|
|
29
|
+
export function toResult(
|
|
30
|
+
c,
|
|
31
|
+
score,
|
|
32
|
+
reason,
|
|
33
|
+
matchedTerms,
|
|
34
|
+
queryTerms,
|
|
35
|
+
outside = false,
|
|
36
|
+
) {
|
|
37
|
+
const base = {
|
|
38
|
+
domain: c.domain,
|
|
39
|
+
name: c._resultName ?? c.name,
|
|
40
|
+
package: c._package,
|
|
41
|
+
score,
|
|
42
|
+
reason,
|
|
43
|
+
description: c.description || '',
|
|
44
|
+
};
|
|
45
|
+
let result;
|
|
46
|
+
switch (c.domain) {
|
|
47
|
+
case 'component':
|
|
48
|
+
result = {
|
|
49
|
+
...base,
|
|
50
|
+
import: c._import,
|
|
51
|
+
command: c._command ?? `astryx component ${c.name}`,
|
|
52
|
+
};
|
|
53
|
+
break;
|
|
54
|
+
case 'hook':
|
|
55
|
+
result = {
|
|
56
|
+
...base,
|
|
57
|
+
import: c._import,
|
|
58
|
+
command: `astryx hook ${c.name}`,
|
|
59
|
+
};
|
|
60
|
+
break;
|
|
61
|
+
case 'doc':
|
|
62
|
+
// A doc result names its topic or route, plus the section when the
|
|
63
|
+
// hit is one section; its command reads exactly that part.
|
|
64
|
+
result = {
|
|
65
|
+
...base,
|
|
66
|
+
name: c._topic ?? c.name,
|
|
67
|
+
...(c._section ? {section: c._section} : {}),
|
|
68
|
+
title: c._title,
|
|
69
|
+
command: c._command ?? `astryx docs ${c.name}`,
|
|
70
|
+
...(c._parent ? {parent: c._parent} : {}),
|
|
71
|
+
};
|
|
72
|
+
break;
|
|
73
|
+
case 'template':
|
|
74
|
+
result = {
|
|
75
|
+
...base,
|
|
76
|
+
displayName: c._displayName,
|
|
77
|
+
kind: c._kind,
|
|
78
|
+
command: `astryx template ${c._commandName ?? c.name} --type ${c._kind}`,
|
|
79
|
+
};
|
|
80
|
+
break;
|
|
81
|
+
case 'theme':
|
|
82
|
+
result = {
|
|
83
|
+
...base,
|
|
84
|
+
displayName: c._displayName,
|
|
85
|
+
command: outside
|
|
86
|
+
? `astryx theme eject ${c.name}`
|
|
87
|
+
: `astryx theme add ${c.name}`,
|
|
88
|
+
};
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
result = base;
|
|
92
|
+
}
|
|
93
|
+
return setResultCoverage(result, matchedTerms, queryTerms);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Throws the search argument errors: an empty query, an unknown type, a limit
|
|
98
|
+
* that is not a positive integer.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} query
|
|
101
|
+
* @param {{type?: string, limit?: number}} options
|
|
102
|
+
*/
|
|
103
|
+
export function checkSearchArgs(query, options) {
|
|
104
|
+
const {type, limit = 20} = options;
|
|
105
|
+
|
|
106
|
+
if (!query || !String(query).trim()) {
|
|
107
|
+
throw new AstryxError(
|
|
108
|
+
'A search query is required',
|
|
109
|
+
[{name: 'astryx search button', reason: 'example'}],
|
|
110
|
+
ERROR_CODES.ERR_INVALID_ARGUMENT,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (type && !SEARCH_DOMAINS.includes(type)) {
|
|
115
|
+
throw new AstryxError(
|
|
116
|
+
`Unknown --type "${type}"`,
|
|
117
|
+
SEARCH_DOMAINS.map(d => ({name: d, reason: 'valid type'})),
|
|
118
|
+
ERROR_CODES.ERR_INVALID_ARGUMENT,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Validate limit here (not just in the CLI) so direct API callers get the same
|
|
123
|
+
// contract: a non-positive or non-integer limit is an error, never a silent
|
|
124
|
+
// "return everything". (Previously `limit <= 0` fell through to the full set.)
|
|
125
|
+
if (limit != null && (!Number.isInteger(limit) || limit <= 0)) {
|
|
126
|
+
throw new AstryxError(
|
|
127
|
+
`Invalid limit "${limit}". Must be a positive integer.`,
|
|
128
|
+
undefined,
|
|
129
|
+
ERROR_CODES.ERR_INVALID_ARGUMENT,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The components each search response was scored against, by response:
|
|
136
|
+
* `build` reads them to tell a part of a page from a page without gathering
|
|
137
|
+
* them again. Module-private, so they never enter search's JSON.
|
|
138
|
+
* @type {WeakMap<object, {name: string, keywords: string[]}[]>}
|
|
139
|
+
*/
|
|
140
|
+
export const searchedComponentsOf = new WeakMap();
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The components (name and keywords) a search response was scored against, or
|
|
144
|
+
* null when that search was narrowed away from components.
|
|
145
|
+
* @param {object} response
|
|
146
|
+
* @returns {{name: string, keywords: string[]}[] | null}
|
|
147
|
+
*/
|
|
148
|
+
export function searchedComponents(response) {
|
|
149
|
+
return searchedComponentsOf.get(response) ?? null;
|
|
150
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
* Best score for a token against a candidate, fanning out through synonyms
|
|
6
|
+
* (synonym hits are discounted so a direct hit always wins).
|
|
7
|
+
* @param {string} tok
|
|
8
|
+
* @param {Candidate} candidate
|
|
9
|
+
* @param {{fuzzy?: boolean}} [opts]
|
|
10
|
+
* @returns {{score: number, reason: string} | null}
|
|
11
|
+
*/
|
|
12
|
+
export function bestForToken(tok: string, candidate: Candidate, opts?: {
|
|
13
|
+
fuzzy?: boolean;
|
|
14
|
+
}): {
|
|
15
|
+
score: number;
|
|
16
|
+
reason: string;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} term - Lowercased full query.
|
|
20
|
+
* @param {string[]} tokens - Content tokens from tokenizeQuery(term).
|
|
21
|
+
* @param {Candidate} candidate
|
|
22
|
+
* @param {string[]} [words] - The query's words for compound names (see
|
|
23
|
+
* compoundHit); empty when the query names an entry outright.
|
|
24
|
+
* @param {{plain?: boolean}} [opts] - `plain`: score without matches on part
|
|
25
|
+
* of a name or a joined keyword.
|
|
26
|
+
* @returns {{score: number, reason: string, matched: number, total: number, alias?: 'partial' | 'keyword', aliasWords?: number} | null}
|
|
27
|
+
* `matched`/`total` are the query concepts this candidate answered, out of
|
|
28
|
+
* the concepts the query had. Callers that must distinguish "matched one word
|
|
29
|
+
* of three" from "matched all three" — `build`, which gates its pages group
|
|
30
|
+
* on coverage — cannot recover that from the score, because a single strong
|
|
31
|
+
* hit and a broad weak one land on the same number.
|
|
32
|
+
*/
|
|
33
|
+
export function scoreQuery(term: string, tokens: string[], candidate: Candidate, words?: string[], { plain }?: {
|
|
34
|
+
plain?: boolean;
|
|
35
|
+
}): {
|
|
36
|
+
score: number;
|
|
37
|
+
reason: string;
|
|
38
|
+
matched: number;
|
|
39
|
+
total: number;
|
|
40
|
+
alias?: "partial" | "keyword";
|
|
41
|
+
aliasWords?: number;
|
|
42
|
+
} | null;
|
|
43
|
+
/**
|
|
44
|
+
* The score of a candidate that matches every content word of a multi-word
|
|
45
|
+
* query, before a bonus of up to 8 for how strong its strongest match is: just
|
|
46
|
+
* above anything that matches only some of the words. The token-sum path tops
|
|
47
|
+
* out near 150 for a partial match (a 100 on one word, the per-word bonus, and
|
|
48
|
+
* the coverage term), so an AND-match with one keyword-strength hit (see
|
|
49
|
+
* {@link STRONG_TOKEN_SCORE}) always outranks an OR-match, and stays below the
|
|
50
|
+
* title tiers.
|
|
51
|
+
*/
|
|
52
|
+
export const FULL_COVERAGE_SCORE: 151;
|
|
53
|
+
/**
|
|
54
|
+
* The strongest single-word hit an every-word match needs to take that tier: a
|
|
55
|
+
* keyword substring. Two passing mentions in prose, or the components a page
|
|
56
|
+
* happens to render, are breadth, not relevance; they stay on the token sum,
|
|
57
|
+
* below an exact name or keyword hit on one of the words.
|
|
58
|
+
*/
|
|
59
|
+
export const STRONG_TOKEN_SCORE: 70;
|
|
60
|
+
export type Candidate = import("./_lexical.mjs").Candidate;
|
|
@@ -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]}
|