@astryxdesign/cli 0.7.2-canary.fe2c655 → 0.7.2
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/CHANGELOG.md +119 -0
- package/README.md +175 -154
- package/api/build/_adapter.d.mts +8 -0
- package/api/build/_adapter.mjs +12 -1
- package/api/build/build.doc.mjs +4 -0
- package/api/build/help/help.mjs +1 -1
- package/api/build/kit/kit.mjs +3 -11
- package/api/component/_adapter.d.mts +72 -1
- package/api/component/_adapter.mjs +212 -18
- package/api/component/component.doc.mjs +6 -1
- package/api/component/component.mjs +82 -15
- package/api/component/component.test.mjs +80 -0
- package/api/component/doc-links.test.mjs +386 -0
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/component/list/list.mjs +42 -4
- package/api/discover/_adapter.d.mts +132 -12
- package/api/discover/_adapter.mjs +488 -27
- package/api/discover/_adapter.test.mjs +599 -17
- package/api/discover/_catalog-view.d.mts +16 -5
- package/api/discover/_catalog-view.mjs +32 -8
- package/api/discover/detail/doc/doc.mjs +9 -1
- package/api/discover/discover.d.mts +11 -0
- package/api/discover/discover.doc.mjs +9 -7
- package/api/discover/discover.mjs +121 -19
- package/api/discover/discover.type.d.mts +20 -7
- package/api/discover/discover.type.mjs +17 -7
- package/api/discover/list/list.d.mts +4 -3
- package/api/discover/list/list.mjs +17 -10
- package/api/docs/_adapter.d.mts +74 -0
- package/api/docs/_adapter.mjs +553 -16
- package/api/docs/docs.doc.mjs +2 -1
- package/api/docs/docs.mjs +2 -1
- package/api/docs/docs.test.mjs +5 -2
- package/api/docs/integration-tree.test.mjs +22 -0
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/docs/list/list.mjs +12 -2
- package/api/docs/reference-blocks.test.mjs +1 -3
- package/api/doctor/_adapter.d.mts +108 -0
- package/api/doctor/_adapter.mjs +223 -4
- package/api/doctor/doctor.d.mts +199 -0
- package/api/doctor/doctor.doc.mjs +3 -2
- package/api/doctor/doctor.mjs +530 -34
- package/api/doctor/doctor.test.mjs +180 -14
- package/api/doctor/release-checks.test.mjs +156 -0
- package/api/doctor/theme-checks.mjs +17 -4
- package/api/doctor/theme-checks.test.mjs +223 -7
- package/api/error.d.mts +10 -0
- package/api/error.mjs +25 -0
- package/api/gap-report/gap-report.d.mts +3 -0
- package/api/gap-report/gap-report.doc.mjs +11 -3
- package/api/gap-report/gap-report.mjs +219 -52
- package/api/gap-report/gap-report.test.mjs +233 -0
- package/api/gap-report/gap-report.type.d.mts +41 -0
- package/api/gap-report/gap-report.type.mjs +18 -0
- package/api/hook/_adapter.mjs +3 -11
- package/api/hook/hook.doc.mjs +4 -0
- package/api/hook/list/list.d.mts +1 -1
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- package/api/init/_adapter.d.mts +22 -0
- package/api/init/_adapter.mjs +59 -0
- package/api/init/init.d.mts +7 -5
- package/api/init/init.doc.mjs +28 -7
- package/api/init/init.mjs +28 -4
- package/api/init/init.type.d.mts +48 -3
- package/api/init/init.type.mjs +37 -3
- package/api/init/run/run.d.mts +1 -1
- package/api/init/run/run.mjs +17 -6
- package/api/init/user/user.d.mts +14 -0
- package/api/init/user/user.mjs +87 -0
- package/api/init/user/user.test.mjs +232 -0
- package/api/integration/_adapter.d.mts +45 -0
- package/api/integration/_adapter.mjs +112 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +8 -4
- package/api/integration/add-contribution.test.mjs +4 -2
- package/api/integration/authoring-checks.mjs +110 -6
- package/api/integration/authoring-checks.test.mjs +808 -0
- package/api/integration/authoring-checks.type.d.mts +143 -0
- package/api/integration/authoring-checks.type.mjs +74 -2
- package/api/integration/check/check.d.mts +96 -0
- package/api/integration/check/check.mjs +357 -0
- package/api/integration/check/check.test.mjs +489 -0
- package/api/integration/integration-check.type.d.mts +118 -0
- package/api/integration/integration-check.type.mjs +84 -0
- package/api/integration/integrationCheck.doc.d.mts +10 -0
- package/api/integration/integrationCheck.doc.mjs +73 -0
- package/api/integration/integrationComponentConflicts.doc.mjs +45 -6
- package/api/integration/integrationDocConflicts.doc.mjs +22 -5
- package/api/integration/pack-check.mjs +364 -120
- package/api/integration/pack-check.test.mjs +730 -10
- package/api/integration/validate-integration-fixes.test.mjs +67 -0
- package/api/integration/validate-integration.d.mts +8 -0
- package/api/integration/validate-integration.mjs +63 -9
- package/api/integration/validate-integration.test.mjs +88 -8
- package/api/integration/validate-integration.type.d.mts +16 -0
- package/api/integration/validate-integration.type.mjs +14 -1
- package/api/integration/validate-unread-theme-folders.test.mjs +3 -2
- package/api/integration/validateIntegration.doc.mjs +19 -3
- package/api/search/_adapter.d.mts +11 -0
- package/api/search/_adapter.mjs +25 -0
- package/api/search/search.d.mts +82 -13
- package/api/search/search.doc.mjs +8 -3
- package/api/search/search.domain-priority.test.mjs +108 -18
- package/api/search/search.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +645 -64
- package/api/search/search.test.mjs +179 -1
- package/api/search/search.type.d.mts +14 -1
- package/api/search/search.type.mjs +6 -1
- package/api/swizzle/_adapter.mjs +2 -2
- package/api/swizzle/copy/copy.mjs +29 -15
- package/api/template/template-integration.test.mjs +1 -1
- package/api/template/template.test.mjs +120 -3
- package/api/theme/_adapter.d.mts +16 -0
- package/api/theme/_adapter.mjs +32 -5
- package/api/theme/build/build.d.mts +2 -1
- package/api/theme/build/build.family.test.mjs +64 -0
- package/api/theme/build/build.mjs +250 -38
- package/api/theme/build/build.packed-old-core.test.mjs +20 -3
- package/api/theme/build/font-warning.mjs +5 -3
- package/api/theme/build/font-warning.test.mjs +2 -1
- package/api/theme/remove/remove.test.mjs +2 -2
- package/api/theme/targets/targets.mjs +3 -10
- package/api/theme/themeBuild.doc.mjs +4 -4
- package/api/theme/themeTargets.doc.mjs +4 -0
- package/api/theme/use/use.test.mjs +1 -1
- package/api/upgrade/_adapter.d.mts +40 -2
- package/api/upgrade/_adapter.mjs +118 -5
- package/api/upgrade/run/cli-behind-core.test.mjs +438 -0
- package/api/upgrade/run/run.mjs +35 -2
- package/api/upgrade/status/status.d.mts +21 -5
- package/api/upgrade/status/status.mjs +45 -8
- package/api/upgrade/upgrade.doc.mjs +6 -3
- package/api/upgrade/upgrade.mjs +5 -1
- package/api/upgrade/upgrade.type.d.mts +23 -0
- package/api/upgrade/upgrade.type.mjs +16 -1
- package/assets/codemods/__tests__/registry.test.mjs +1 -0
- package/assets/codemods/registry.mjs +1 -0
- package/assets/codemods/runner.mjs +7 -1
- package/assets/codemods/transforms/v0.7.2/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
- package/assets/codemods/transforms/v0.7.2/index.mjs +20 -0
- package/assets/codemods/transforms/v0.7.2/migrate-list-edge-compensation-to-edge.mjs +198 -0
- package/assets/discover/official-packages.json +367 -0
- package/assets/docs/README.md +10 -0
- package/assets/docs/author-a-theme.doc.mjs +3 -3
- package/assets/docs/browser-support.doc.mjs +1 -1
- package/assets/docs/getting-started.doc.mjs +26 -6
- package/assets/docs/styling-overview.doc.mjs +1 -1
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +2 -2
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +22 -2
- package/assets/docs/tree/add-a-theme.doc.mjs +13 -3
- package/assets/docs/tree/add-a-topic.doc.mjs +54 -9
- package/assets/docs/tree/agent-guidance.doc.mjs +19 -5
- package/assets/docs/tree/block-template.doc.mjs +8 -20
- package/assets/docs/tree/build-the-template.doc.mjs +1 -1
- package/assets/docs/tree/building-blocks.doc.mjs +4 -0
- package/assets/docs/tree/check-the-props.doc.mjs +201 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +69 -11
- package/assets/docs/tree/checks.doc.mjs +32 -24
- package/assets/docs/tree/ci-and-containers.doc.mjs +88 -0
- package/assets/docs/tree/cli-command.doc.mjs +142 -0
- package/assets/docs/tree/cli.doc.mjs +6 -0
- package/assets/docs/tree/codemods.doc.mjs +17 -7
- package/assets/docs/tree/commands.doc.mjs +7 -1
- package/assets/docs/tree/component-family.doc.mjs +8 -6
- package/assets/docs/tree/component-imports.doc.mjs +21 -3
- package/assets/docs/tree/components.doc.mjs +7 -1
- package/assets/docs/tree/computer-agent-guide.doc.mjs +160 -0
- package/assets/docs/tree/computer-integrations.doc.mjs +93 -0
- package/assets/docs/tree/configuration.doc.mjs +7 -3
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +41 -22
- package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
- package/assets/docs/tree/describe-the-component.doc.mjs +3 -3
- package/assets/docs/tree/docs.doc.mjs +9 -3
- package/assets/docs/tree/document-the-template.doc.mjs +1 -1
- package/assets/docs/tree/document-the-theme.doc.mjs +3 -3
- package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
- package/assets/docs/tree/extend-or-replace.doc.mjs +19 -9
- package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
- package/assets/docs/tree/generate-a-palette.doc.mjs +7 -5
- package/assets/docs/tree/global-integrations.doc.mjs +124 -0
- package/assets/docs/tree/global.doc.mjs +131 -0
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +5 -5
- package/assets/docs/tree/install-once.doc.mjs +105 -0
- package/assets/docs/tree/integrations.doc.mjs +13 -1
- package/assets/docs/tree/layout-spacing.doc.mjs +2 -3
- package/assets/docs/tree/layout.doc.mjs +6 -6
- package/assets/docs/tree/links.doc.mjs +7 -6
- package/assets/docs/tree/migration-setup.doc.mjs +3 -2
- package/assets/docs/tree/migration-verification.doc.mjs +0 -1
- package/assets/docs/tree/migration.doc.mjs +0 -1
- package/assets/docs/tree/monorepos.doc.mjs +84 -0
- package/assets/docs/tree/package-and-test.doc.mjs +1 -1
- package/assets/docs/tree/page-template.doc.mjs +4 -8
- package/assets/docs/tree/problems.doc.mjs +205 -0
- package/assets/docs/tree/project-global-or-both.doc.mjs +131 -0
- package/assets/docs/tree/publishing.doc.mjs +6 -6
- package/assets/docs/tree/quick-start.doc.mjs +53 -27
- package/assets/docs/tree/reference.doc.mjs +192 -0
- package/assets/docs/tree/replace-a-core-component.doc.mjs +15 -7
- package/assets/docs/tree/replace-a-core-template.doc.mjs +10 -12
- package/assets/docs/tree/scaffold.doc.mjs +1 -1
- package/assets/docs/tree/sections-and-placement.doc.mjs +9 -11
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +17 -5
- package/assets/docs/tree/short-and-findable.doc.mjs +9 -8
- package/assets/docs/tree/single-component.doc.mjs +15 -6
- package/assets/docs/tree/start-a-template.doc.mjs +18 -8
- package/assets/docs/tree/subcomponent.doc.mjs +6 -6
- package/assets/docs/tree/template-assets.doc.mjs +2 -2
- package/assets/docs/tree/template-doc-overview.doc.mjs +6 -17
- package/assets/docs/tree/template-fonts.doc.mjs +1 -1
- 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 +2 -2
- package/assets/docs/tree/templates.doc.mjs +7 -1
- package/assets/docs/tree/test-in-an-app.doc.mjs +7 -7
- package/assets/docs/tree/test-template-in-app.doc.mjs +5 -5
- package/assets/docs/tree/themes.doc.mjs +4 -0
- package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
- package/assets/docs/tree/troubleshooting.doc.mjs +29 -13
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/uninstall.doc.mjs +109 -0
- package/assets/docs/tree/updates.doc.mjs +92 -0
- package/assets/docs/tree/upgrading.doc.mjs +6 -6
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +14 -2
- package/assets/docs/tree/verify-packed-template.doc.mjs +3 -3
- package/assets/docs/tree/versioning.doc.mjs +10 -9
- package/assets/docs/tree/who-answers.doc.mjs +119 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +7 -6
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/docs/working-with-ai.doc.mjs +66 -13
- package/assets/theme.template.ts +1 -1
- package/authoring/codemod/codemod.doc.mjs +7 -3
- package/authoring/config/config.doc.mjs +9 -6
- package/authoring/config/type.ts +4 -2
- package/authoring/debug/parse.d.mts +5 -5
- package/authoring/discover/discover.doc.mjs +7 -1
- package/authoring/doctypes/_schema.d.mts +8 -3
- package/authoring/doctypes/_schema.mjs +1 -0
- package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
- package/authoring/doctypes/command/command.doc.mjs +20 -12
- package/authoring/doctypes/command/type.ts +14 -0
- package/authoring/doctypes/component/component.doc.mjs +30 -27
- package/authoring/doctypes/component/type.ts +21 -8
- package/authoring/doctypes/enum/enum.doc.mjs +7 -4
- package/authoring/doctypes/function/function.doc.mjs +9 -8
- package/authoring/doctypes/hook/hook.doc.mjs +5 -4
- package/authoring/doctypes/hook/type.ts +4 -0
- package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
- package/authoring/doctypes/reference/reference.doc.mjs +12 -11
- package/authoring/doctypes/schema/schema.doc.mjs +9 -9
- package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
- package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
- package/authoring/doctypes/template/parse.d.mts +6 -2
- package/authoring/doctypes/template/parse.mjs +3 -1
- package/authoring/doctypes/template/template.doc.mjs +3 -3
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/doctypes/theme/theme.doc.mjs +2 -2
- package/authoring/gap-report/gap-report.doc.mjs +2 -2
- package/authoring/identity/identity.doc.mjs +3 -3
- package/authoring/index.d.ts +2 -0
- package/authoring/integration/integration.doc.mjs +30 -6
- package/authoring/integration/parse.test.mjs +21 -0
- package/authoring/integration/schema.d.mts +16 -3
- package/authoring/integration/schema.mjs +34 -2
- package/authoring/integration/type.ts +12 -0
- package/clients/cli/bin/astryx.mjs +20 -3
- package/clients/cli/cli-command.test.mjs +166 -0
- package/clients/cli/commands/blog.doc.mjs +1 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +17 -3
- package/clients/cli/commands/build-theme.font-warning.test.mjs +3 -3
- package/clients/cli/commands/build-theme.mjs +26 -13
- package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
- package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
- package/clients/cli/commands/build.doc.mjs +1 -0
- package/clients/cli/commands/build.exit-codes-doc.test.mjs +4 -0
- package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
- package/clients/cli/commands/component.doc.mjs +1 -0
- package/clients/cli/commands/discover.broken-integration.test.mjs +11 -7
- package/clients/cli/commands/discover.doc.mjs +9 -1
- package/clients/cli/commands/discover.mjs +144 -93
- package/clients/cli/commands/discover.no-source.test.mjs +448 -33
- package/clients/cli/commands/discover.sources.test.mjs +20 -1
- package/clients/cli/commands/docs.doc.mjs +1 -0
- package/clients/cli/commands/docs.mjs +49 -19
- package/clients/cli/commands/docs.test.mjs +2 -2
- package/clients/cli/commands/doctor-integration-components.doc.mjs +19 -4
- package/clients/cli/commands/doctor-integration-docs.doc.mjs +20 -5
- package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -0
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +22 -2
- package/clients/cli/commands/doctor-integration.test.mjs +888 -3
- package/clients/cli/commands/doctor.doc.mjs +12 -2
- package/clients/cli/commands/doctor.mjs +292 -7
- package/clients/cli/commands/doctor.test.mjs +10 -4
- package/clients/cli/commands/gap-report.doc.mjs +15 -2
- package/clients/cli/commands/gap-report.mjs +24 -2
- package/clients/cli/commands/gap-report.test.mjs +79 -0
- package/clients/cli/commands/hook.doc.mjs +1 -0
- package/clients/cli/commands/init.behavior.test.mjs +5 -0
- package/clients/cli/commands/init.doc.mjs +29 -3
- package/clients/cli/commands/integration-add.doc.mjs +7 -0
- package/clients/cli/commands/integration-authoring.test.mjs +49 -3
- package/clients/cli/commands/integration-check.doc.mjs +93 -0
- package/clients/cli/commands/integration-check.test.mjs +441 -0
- package/clients/cli/commands/integration-pack.doc.mjs +8 -1
- package/clients/cli/commands/integration-verify.doc.mjs +8 -1
- package/clients/cli/commands/integration.doc.mjs +6 -2
- package/clients/cli/commands/integration.mjs +119 -7
- package/clients/cli/commands/json-contract.test.mjs +11 -2
- package/clients/cli/commands/manifest.doc.mjs +1 -0
- package/clients/cli/commands/outside-project.test.mjs +774 -0
- package/clients/cli/commands/search.doc.mjs +1 -0
- package/clients/cli/commands/search.mjs +16 -11
- package/clients/cli/commands/search.test.mjs +429 -296
- package/clients/cli/commands/setup-nudge.test.mjs +102 -2
- package/clients/cli/commands/swizzle.doc.mjs +7 -0
- package/clients/cli/commands/swizzle.mjs +5 -3
- package/clients/cli/commands/swizzle.routing.test.mjs +19 -2
- package/clients/cli/commands/template.doc.mjs +1 -0
- package/clients/cli/commands/text-json-parity.test.mjs +5 -0
- package/clients/cli/commands/theme-add-lifecycle.test.mjs +3 -3
- package/clients/cli/commands/theme-add.doc.mjs +8 -1
- package/clients/cli/commands/theme-build.doc.mjs +3 -2
- package/clients/cli/commands/theme-eject.doc.mjs +1 -0
- package/clients/cli/commands/theme-list.behavior.test.mjs +6 -0
- package/clients/cli/commands/theme-list.doc.mjs +1 -0
- package/clients/cli/commands/theme-palette-generate.doc.mjs +1 -0
- package/clients/cli/commands/theme-remove.doc.mjs +7 -0
- package/clients/cli/commands/theme-targets.doc.mjs +1 -0
- package/clients/cli/commands/theme-template.doc.mjs +1 -0
- package/clients/cli/commands/theme-use.doc.mjs +7 -0
- package/clients/cli/commands/upgrade.config-ordering.test.mjs +2 -1
- package/clients/cli/commands/upgrade.doc.mjs +22 -1
- package/clients/cli/commands/upgrade.file-protection.test.mjs +3 -2
- package/clients/cli/commands/upgrade.hook-output.test.mjs +2 -1
- package/clients/cli/commands/upgrade.older-cli.test.mjs +160 -0
- package/clients/cli/index.mjs +112 -16
- package/clients/cli/lib/cli-error.mjs +4 -1
- package/clients/cli/lib/component-format.test.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +2 -2
- package/clients/cli/lib/json-shim.mjs +3 -1
- package/clients/cli/lib/manifest.d.ts +9 -0
- package/clients/cli/lib/manifest.mjs +17 -1
- package/clients/cli/lib/manifest.test.mjs +25 -0
- package/foundation/agent-docs/agent-doc-state.d.mts +35 -6
- package/foundation/agent-docs/agent-doc-state.mjs +66 -6
- package/foundation/agent-docs/agent-docs.d.mts +4 -3
- package/foundation/agent-docs/agent-docs.mjs +11 -9
- package/foundation/agent-docs/computer-agent-text.d.mts +133 -0
- package/foundation/agent-docs/computer-agent-text.mjs +623 -0
- package/foundation/agent-docs/computer-agent-text.test.mjs +602 -0
- package/foundation/config/project.d.mts +35 -3
- package/foundation/config/project.mjs +226 -7
- package/foundation/config/project.test.mjs +66 -0
- package/foundation/config/theme-state.mjs +5 -4
- package/foundation/discovery/authoring-self-docs.mjs +2 -1
- package/foundation/discovery/cli-self-docs.mjs +20 -3
- package/foundation/discovery/cli-self-docs.test.mjs +3 -2
- package/foundation/discovery/component-discovery.d.mts +190 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +864 -56
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- package/foundation/discovery/component-replacement.mjs +20 -1
- package/foundation/discovery/component-replacement.test.mjs +40 -0
- package/foundation/discovery/copied-passages.d.mts +216 -0
- package/foundation/discovery/copied-passages.mjs +720 -0
- package/foundation/discovery/copied-passages.test.mjs +124 -0
- package/foundation/discovery/doc-consistency.d.mts +34 -0
- package/foundation/discovery/doc-consistency.mjs +513 -0
- package/foundation/discovery/doc-consistency.test.mjs +347 -0
- package/foundation/discovery/doc-fluff.d.mts +112 -0
- package/foundation/discovery/doc-fluff.mjs +328 -0
- package/foundation/discovery/doc-fluff.test.mjs +142 -0
- package/foundation/discovery/doc-types.d.mts +157 -0
- package/foundation/discovery/doc-types.mjs +1231 -0
- package/foundation/discovery/doc-types.test.mjs +744 -0
- package/foundation/discovery/docs-discovery.d.mts +88 -0
- package/foundation/discovery/docs-discovery.mjs +83 -2
- package/foundation/discovery/integration-doc-files.d.mts +22 -0
- package/foundation/discovery/integration-doc-files.mjs +231 -0
- package/foundation/discovery/integration-doc-files.test.mjs +246 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +32 -0
- package/foundation/discovery/project-typescript.mjs +89 -0
- package/foundation/discovery/prop-drift.d.mts +297 -0
- package/foundation/discovery/prop-drift.mjs +2428 -0
- package/foundation/discovery/prop-drift.test.mjs +2337 -0
- package/foundation/discovery/runner-allowlist.d.mts +40 -0
- package/foundation/discovery/runner-allowlist.mjs +53 -0
- package/foundation/discovery/stray-pointers.d.mts +52 -0
- package/foundation/discovery/stray-pointers.mjs +293 -0
- package/foundation/discovery/stray-pointers.test.mjs +86 -0
- package/foundation/discovery/template-adapter.mjs +74 -5
- package/foundation/discovery/template-needs.mjs +5 -2
- package/foundation/discovery/theme-imports.mjs +8 -4
- package/foundation/discovery/theme-imports.test.mjs +1 -1
- package/foundation/discovery/type-match.d.mts +53 -0
- package/foundation/discovery/type-match.mjs +660 -0
- package/foundation/discovery/type-match.test.mjs +169 -0
- package/foundation/discovery/type-text.d.mts +68 -0
- package/foundation/discovery/type-text.mjs +658 -0
- package/foundation/discovery/type-text.test.mjs +196 -0
- package/foundation/discovery/unresolved-references.d.mts +62 -0
- package/foundation/discovery/unresolved-references.mjs +790 -0
- package/foundation/discovery/unresolved-references.test.mjs +567 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +51 -1
- package/foundation/doc-compiler/inputs.mjs +27 -1
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/doc-compiler/links.d.mts +41 -5
- package/foundation/doc-compiler/links.mjs +167 -28
- package/foundation/doc-compiler/links.test.mjs +97 -4
- package/foundation/doc-compiler/read.d.mts +1 -1
- package/foundation/doc-compiler/tree.mjs +30 -1
- package/foundation/doc-compiler/tree.test.mjs +8 -1
- package/foundation/env/cli-command.d.mts +83 -0
- package/foundation/env/cli-command.mjs +152 -0
- package/foundation/env/cli-command.test.mjs +129 -0
- package/foundation/env/node-version.test.mjs +15 -3
- package/foundation/env/package-manager.d.mts +93 -9
- package/foundation/env/package-manager.global.test.mjs +66 -0
- package/foundation/env/package-manager.mjs +184 -13
- package/foundation/env/package-manager.test.mjs +341 -15
- package/foundation/env/project-scope.d.mts +9 -0
- package/foundation/env/project-scope.mjs +33 -0
- package/foundation/env/search-path.d.mts +42 -0
- package/foundation/env/search-path.mjs +205 -0
- package/foundation/env/search-path.test.mjs +122 -0
- package/foundation/env/user-dirs.d.mts +39 -0
- package/foundation/env/user-dirs.mjs +93 -0
- package/foundation/env/user-dirs.test.mjs +72 -0
- package/foundation/env/user-settings.d.mts +46 -0
- package/foundation/env/user-settings.mjs +106 -0
- package/foundation/env/user-settings.test.mjs +64 -0
- package/foundation/fs/file-protection.mjs +15 -1
- package/foundation/fs/module-loader.d.mts +2 -1
- package/foundation/fs/module-loader.mjs +60 -4
- package/foundation/fs/module-loader.stdout.test.mjs +3 -2
- package/foundation/fs/module-loader.test.mjs +194 -0
- package/foundation/fs/path-compare.d.mts +33 -0
- package/foundation/fs/path-compare.mjs +73 -0
- package/foundation/fs/paths.d.mts +57 -1
- package/foundation/fs/paths.mjs +131 -3
- package/foundation/fs/paths.test.mjs +10 -1
- package/foundation/integrations/computer-integrations.d.mts +78 -0
- package/foundation/integrations/computer-integrations.mjs +316 -0
- package/foundation/integrations/computer-integrations.test.mjs +292 -0
- package/foundation/integrations/contribution-inventory.d.mts +11 -0
- package/foundation/integrations/contribution-inventory.mjs +56 -3
- package/foundation/integrations/integration-warnings.mjs +3 -2
- package/foundation/integrations/integration-warnings.test.mjs +8 -9
- package/foundation/integrations/integrations.d.mts +25 -1
- package/foundation/integrations/integrations.mjs +46 -3
- package/foundation/integrations/issue.ts +6 -0
- package/foundation/integrations/validate-contributions.d.mts +2 -0
- package/foundation/integrations/validate-contributions.mjs +41 -1
- package/foundation/release/agent-text-worker.d.mts +4 -0
- package/foundation/release/agent-text-worker.mjs +52 -0
- package/foundation/release/cache.d.mts +310 -0
- package/foundation/release/cache.mjs +1216 -0
- package/foundation/release/cache.test.mjs +920 -0
- package/foundation/release/choose.d.mts +207 -0
- package/foundation/release/choose.mjs +677 -0
- package/foundation/release/choose.test.mjs +427 -0
- package/foundation/release/core-worker.d.mts +4 -0
- package/foundation/release/core-worker.mjs +38 -0
- package/foundation/release/global-state.d.mts +259 -0
- package/foundation/release/global-state.mjs +472 -0
- package/foundation/release/global-state.test.mjs +381 -0
- package/foundation/release/handoff.d.mts +60 -0
- package/foundation/release/handoff.e2e.test.mjs +278 -0
- package/foundation/release/handoff.mjs +357 -0
- package/foundation/release/lock-lease.d.mts +23 -0
- package/foundation/release/lock-lease.mjs +80 -0
- package/foundation/release/notices.d.mts +253 -0
- package/foundation/release/notices.mjs +596 -0
- package/foundation/release/notices.test.mjs +565 -0
- package/foundation/release/npm-guard.d.mts +4 -0
- package/foundation/release/npm-guard.mjs +126 -0
- package/foundation/release/project.d.mts +141 -0
- package/foundation/release/project.mjs +466 -0
- package/foundation/release/project.test.mjs +288 -0
- package/foundation/release/refresh-worker.d.mts +4 -0
- package/foundation/release/refresh-worker.mjs +27 -0
- package/foundation/release/run-context.d.mts +47 -0
- package/foundation/release/run-context.mjs +149 -0
- package/foundation/release/update-notice-env.d.mts +28 -0
- package/foundation/release/update-notice-env.mjs +39 -0
- package/foundation/response/base.d.ts +26 -3
- package/foundation/response/error-codes.d.mts +3 -1
- package/foundation/response/error-codes.doc.mjs +12 -0
- package/foundation/response/error-codes.mjs +17 -0
- package/foundation/response/error-codes.test.mjs +2 -0
- package/foundation/response/json-contract.test.mjs +7 -2
- package/foundation/response/json.mjs +4 -1
- package/foundation/response/response-types.doc.mjs +22 -11
- package/foundation/response/response-types.doc.test.mjs +46 -0
- package/foundation/response/response.doc.mjs +3 -3
- package/foundation/xle/browser-imports.test.mjs +74 -0
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +11 -11
- package/scripts/postinstall.mjs +24 -5
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copied passages: prose that two docs repeat word for word. One fact has one
|
|
5
|
+
* owner, so a passage repeated in a second doc goes stale in one of them the
|
|
6
|
+
* next time the other is edited.
|
|
7
|
+
*
|
|
8
|
+
* Only prose is compared: code, examples, names, titles, keywords, and links
|
|
9
|
+
* are left out, and a run must be at least {@link PASSAGE_WORDS} words long,
|
|
10
|
+
* so a reported passage is always a real copy, never a shared phrase.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
|
|
15
|
+
import {builtinGuideFiles, loadTopicModule} from './docs-discovery.mjs';
|
|
16
|
+
|
|
17
|
+
/** The shortest run of identical words reported as a copy. */
|
|
18
|
+
export const PASSAGE_WORDS = 10;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A copy carries at least this many distinct content words, so a run of
|
|
22
|
+
* glue words ("and it is in the way that you can…") is a phrase, not a fact.
|
|
23
|
+
*/
|
|
24
|
+
export const MIN_CONTENT_WORDS = 4;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A run that this many topics share is boilerplate, such as a lead-in every
|
|
28
|
+
* guide of a series opens with, not a copy of one topic's fact.
|
|
29
|
+
*/
|
|
30
|
+
export const BOILERPLATE_TOPICS = 3;
|
|
31
|
+
|
|
32
|
+
/** English function words, which say nothing about what a passage states. */
|
|
33
|
+
const FUNCTION_WORDS = new Set(
|
|
34
|
+
(
|
|
35
|
+
'a about above after again all also an and any are as at be because been before being below between both but by ' +
|
|
36
|
+
'can could did do does doing down during each either else every few for from further had has have having he her here ' +
|
|
37
|
+
'him his how i if in into is it its itself just may me might more most must my no nor not now of off on once only or ' +
|
|
38
|
+
'other our out over own same she should so some such than that the their them then there these they this those through ' +
|
|
39
|
+
'to too under until up us very was we were what when where which while who whom why will with would you your'
|
|
40
|
+
).split(' '),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The number of distinct content words in a run: words that are not function
|
|
45
|
+
* words, with surrounding punctuation set aside.
|
|
46
|
+
*
|
|
47
|
+
* @param {{word: string}[]} words
|
|
48
|
+
* @param {number} start
|
|
49
|
+
* @param {number} length
|
|
50
|
+
*/
|
|
51
|
+
export function contentWords(words, start, length) {
|
|
52
|
+
const found = new Set();
|
|
53
|
+
for (const {word} of words.slice(start, start + length)) {
|
|
54
|
+
const bare = word.replace(/^[^a-z0-9@]+|[^a-z0-9]+$/g, '');
|
|
55
|
+
if (bare && !FUNCTION_WORDS.has(bare)) found.add(bare);
|
|
56
|
+
}
|
|
57
|
+
return found.size;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Fields that hold identifiers or code rather than prose. */
|
|
61
|
+
const SKIP_KEYS = new Set([
|
|
62
|
+
'aliases',
|
|
63
|
+
'audience',
|
|
64
|
+
'category',
|
|
65
|
+
'code',
|
|
66
|
+
'example',
|
|
67
|
+
'examples',
|
|
68
|
+
'extends',
|
|
69
|
+
'id',
|
|
70
|
+
'import',
|
|
71
|
+
'keywords',
|
|
72
|
+
'label',
|
|
73
|
+
'lang',
|
|
74
|
+
'name',
|
|
75
|
+
'placement',
|
|
76
|
+
'projection',
|
|
77
|
+
'props',
|
|
78
|
+
'relationships',
|
|
79
|
+
'replaces',
|
|
80
|
+
'source',
|
|
81
|
+
'title',
|
|
82
|
+
'type',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/** Blocks whose content is code, or another doc's content included by name. */
|
|
86
|
+
const SKIP_BLOCKS = new Set(['code', 'example', 'reference']);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Pointers to other docs, which are not prose: a `{@link …}` and a written
|
|
90
|
+
* `astryx docs <name>` command. Two topics pointing at the same section say
|
|
91
|
+
* no fact twice.
|
|
92
|
+
*/
|
|
93
|
+
const POINTERS = /\{@link [^}]*\}|`astryx docs [^`]*`/g;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The words a passage is compared by: lowercase, with links and Markdown
|
|
97
|
+
* emphasis set aside. A token with no letter or digit (the `.` after a link)
|
|
98
|
+
* is not a word.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} text
|
|
101
|
+
* @returns {string[]}
|
|
102
|
+
*/
|
|
103
|
+
export function passageWords(text) {
|
|
104
|
+
return text
|
|
105
|
+
.toLowerCase()
|
|
106
|
+
.replace(POINTERS, ' ')
|
|
107
|
+
.replace(/[`*]/g, '')
|
|
108
|
+
.split(/[^a-z0-9'@./-]+/)
|
|
109
|
+
.filter(word => /[a-z0-9]/.test(word));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Every word of a doc file, in order, with its 1-based line. Words are runs of
|
|
114
|
+
* letters and digits, so punctuation, code ticks, quotes and escapes in the
|
|
115
|
+
* source neither join nor split them.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} source
|
|
118
|
+
* @returns {{word: string, line: number}[]}
|
|
119
|
+
*/
|
|
120
|
+
export function fileWords(source) {
|
|
121
|
+
/** @type {{word: string, line: number}[]} */
|
|
122
|
+
const words = [];
|
|
123
|
+
source.split('\n').forEach((line, at) => {
|
|
124
|
+
for (const match of line.matchAll(/[A-Za-z0-9]+/g)) {
|
|
125
|
+
words.push({word: match[0].toLowerCase(), line: at + 1});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return words;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The words of a string the way {@link fileWords} reads a file, each with its
|
|
133
|
+
* offset in the string.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} text
|
|
136
|
+
* @returns {{word: string, at: number}[]}
|
|
137
|
+
*/
|
|
138
|
+
export function textWords(text) {
|
|
139
|
+
return [...text.matchAll(/[A-Za-z0-9]+/g)].map(match => ({
|
|
140
|
+
word: match[0].toLowerCase(),
|
|
141
|
+
at: /** @type {number} */ (match.index),
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Where a string's whole run of words starts among a file's words, looking
|
|
147
|
+
* from `from` on, or -1.
|
|
148
|
+
*
|
|
149
|
+
* @param {{word: string}[]} file
|
|
150
|
+
* @param {{word: string}[]} words
|
|
151
|
+
* @param {number} [from]
|
|
152
|
+
* @returns {number}
|
|
153
|
+
*/
|
|
154
|
+
export function locateWords(file, words, from = 0) {
|
|
155
|
+
if (words.length === 0) return -1;
|
|
156
|
+
outer: for (let i = from; i + words.length <= file.length; i++) {
|
|
157
|
+
for (let k = 0; k < words.length; k++) {
|
|
158
|
+
if (file[i + k].word !== words[k].word) continue outer;
|
|
159
|
+
}
|
|
160
|
+
return i;
|
|
161
|
+
}
|
|
162
|
+
return -1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The file line of the character at offset `at` of a string whose words start
|
|
167
|
+
* at `index` among the file's words.
|
|
168
|
+
*
|
|
169
|
+
* @param {{line: number}[]} file
|
|
170
|
+
* @param {number} index
|
|
171
|
+
* @param {{at: number}[]} words the string's words
|
|
172
|
+
* @param {number} at
|
|
173
|
+
* @returns {number | null}
|
|
174
|
+
*/
|
|
175
|
+
export function lineAtOffset(file, index, words, at) {
|
|
176
|
+
let k = words.findIndex(word => word.at + 1 > at);
|
|
177
|
+
if (k < 0) k = words.length - 1;
|
|
178
|
+
return file[index + k]?.line ?? null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A shared run with the boilerplate trimmed off its ends, judged window by
|
|
183
|
+
* window: every word a leading boilerplate window covers, and every word from
|
|
184
|
+
* the first trailing one on. A lead-in many topics open with thus never hides
|
|
185
|
+
* the copy after it. Null when less than {@link PASSAGE_WORDS} words are left.
|
|
186
|
+
*
|
|
187
|
+
* @param {{start: number, length: number}} run
|
|
188
|
+
* @param {(at: number) => boolean} boilerplate whether the window of
|
|
189
|
+
* {@link PASSAGE_WORDS} words starting at `at` is boilerplate
|
|
190
|
+
* @returns {{start: number, length: number} | null}
|
|
191
|
+
*/
|
|
192
|
+
export function trimBoilerplate({start, length}, boilerplate) {
|
|
193
|
+
const lastWindow = start + length - PASSAGE_WORDS;
|
|
194
|
+
let lead = start;
|
|
195
|
+
while (lead <= lastWindow && boilerplate(lead)) lead += 1;
|
|
196
|
+
const begin = lead > start ? lead - 1 + PASSAGE_WORDS : start;
|
|
197
|
+
let tail = lastWindow;
|
|
198
|
+
while (tail >= begin && boilerplate(tail)) tail -= 1;
|
|
199
|
+
const end = tail < lastWindow ? tail + 1 : start + length;
|
|
200
|
+
return end - begin < PASSAGE_WORDS
|
|
201
|
+
? null
|
|
202
|
+
: {start: begin, length: end - begin};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The words of one string as passageWords reads them, each with where it sits
|
|
207
|
+
* in the string as written, so a passage can be quoted with its case,
|
|
208
|
+
* punctuation and code ticks.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} text
|
|
211
|
+
* @returns {{word: string, start: number, end: number}[]}
|
|
212
|
+
*/
|
|
213
|
+
export function wordSpans(text) {
|
|
214
|
+
const masked = text
|
|
215
|
+
.toLowerCase()
|
|
216
|
+
.replace(POINTERS, pointer => ' '.repeat(pointer.length));
|
|
217
|
+
/** @type {{word: string, start: number, end: number}[]} */
|
|
218
|
+
const spans = [];
|
|
219
|
+
for (const match of masked.matchAll(/[a-z0-9'@./`*-]+/g)) {
|
|
220
|
+
const word = match[0].replace(/[`*]/g, '');
|
|
221
|
+
if (!/[a-z0-9]/.test(word)) continue;
|
|
222
|
+
const start = /** @type {number} */ (match.index);
|
|
223
|
+
spans.push({word, start, end: start + match[0].length});
|
|
224
|
+
}
|
|
225
|
+
return spans;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Each prose string of a doc, in order, with the title of the section it is
|
|
230
|
+
* in (null before the first section). Code, examples, reference blocks, and
|
|
231
|
+
* identifier fields are left out.
|
|
232
|
+
*
|
|
233
|
+
* @param {unknown} doc an authored doc object
|
|
234
|
+
* @returns {{text: string, section: string | null}[]}
|
|
235
|
+
*/
|
|
236
|
+
export function proseStrings(doc) {
|
|
237
|
+
/** @type {{text: string, section: string | null}[]} */
|
|
238
|
+
const strings = [];
|
|
239
|
+
/**
|
|
240
|
+
* @param {unknown} value
|
|
241
|
+
* @param {string | null} section
|
|
242
|
+
*/
|
|
243
|
+
const walk = (value, section) => {
|
|
244
|
+
if (typeof value === 'string') {
|
|
245
|
+
strings.push({text: value, section});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
for (const item of value) walk(item, section);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (value == null || typeof value !== 'object') return;
|
|
253
|
+
const record = /** @type {Record<string, unknown>} */ (value);
|
|
254
|
+
if (typeof record.type === 'string' && SKIP_BLOCKS.has(record.type)) return;
|
|
255
|
+
if (Array.isArray(record.sections)) {
|
|
256
|
+
for (const [key, field] of Object.entries(record)) {
|
|
257
|
+
if (key !== 'sections' && !SKIP_KEYS.has(key)) walk(field, section);
|
|
258
|
+
}
|
|
259
|
+
for (const each of record.sections) {
|
|
260
|
+
const title =
|
|
261
|
+
typeof each?.title === 'string' ? each.title : (section ?? null);
|
|
262
|
+
walk(each, title);
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
for (const [key, field] of Object.entries(record)) {
|
|
267
|
+
if (!SKIP_KEYS.has(key)) walk(field, section);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
walk(doc, null);
|
|
271
|
+
return strings;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Each prose word of a doc, in order, with the title of the section it is in
|
|
276
|
+
* (null before the first section) and the number of the string it came from.
|
|
277
|
+
* A passage never runs from one string into the next, so two fields that
|
|
278
|
+
* happen to sit side by side are not read as one sentence.
|
|
279
|
+
*
|
|
280
|
+
* @param {unknown} doc an authored doc object
|
|
281
|
+
* @returns {{word: string, section: string | null, block: number}[]}
|
|
282
|
+
*/
|
|
283
|
+
export function proseWords(doc) {
|
|
284
|
+
/** @type {{word: string, section: string | null, block: number}[]} */
|
|
285
|
+
const words = [];
|
|
286
|
+
proseStrings(doc).forEach(({text, section}, at) => {
|
|
287
|
+
for (const word of passageWords(text)) {
|
|
288
|
+
words.push({word, section, block: at + 1});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return words;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Whether the `size` words from `at` all come from one string.
|
|
296
|
+
* @param {{block?: number}[]} words
|
|
297
|
+
* @param {number} at
|
|
298
|
+
* @param {number} size
|
|
299
|
+
*/
|
|
300
|
+
function inOneString(words, at, size) {
|
|
301
|
+
return words[at].block === words[at + size - 1].block;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The runs of at least `size` words that both docs have, each inside one
|
|
306
|
+
* string of each doc, longest first, as its start in `mine` and its length.
|
|
307
|
+
*
|
|
308
|
+
* @param {{word: string, block?: number}[]} mine
|
|
309
|
+
* @param {{word: string, block?: number}[]} other
|
|
310
|
+
* @param {number} [size]
|
|
311
|
+
* @returns {{start: number, length: number}[]}
|
|
312
|
+
*/
|
|
313
|
+
export function sharedPassages(mine, other, size = PASSAGE_WORDS) {
|
|
314
|
+
if (mine.length < size || other.length < size) return [];
|
|
315
|
+
const theirs = other.map(each => each.word);
|
|
316
|
+
/** @type {Map<string, number[]>} */
|
|
317
|
+
const starts = new Map();
|
|
318
|
+
for (let i = 0; i + size <= theirs.length; i++) {
|
|
319
|
+
if (!inOneString(other, i, size)) continue;
|
|
320
|
+
const key = theirs.slice(i, i + size).join(' ');
|
|
321
|
+
const list = starts.get(key);
|
|
322
|
+
if (list) list.push(i);
|
|
323
|
+
else starts.set(key, [i]);
|
|
324
|
+
}
|
|
325
|
+
const words = mine.map(each => each.word);
|
|
326
|
+
/** @type {{start: number, length: number}[]} */
|
|
327
|
+
const runs = [];
|
|
328
|
+
let i = 0;
|
|
329
|
+
while (i + size <= words.length) {
|
|
330
|
+
const found = inOneString(mine, i, size)
|
|
331
|
+
? starts.get(words.slice(i, i + size).join(' '))
|
|
332
|
+
: undefined;
|
|
333
|
+
if (!found) {
|
|
334
|
+
i++;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
let best = size;
|
|
338
|
+
for (const j of found) {
|
|
339
|
+
let length = size;
|
|
340
|
+
while (
|
|
341
|
+
i + length < words.length &&
|
|
342
|
+
j + length < theirs.length &&
|
|
343
|
+
mine[i + length].block === mine[i].block &&
|
|
344
|
+
other[j + length].block === other[j].block &&
|
|
345
|
+
words[i + length] === theirs[j + length]
|
|
346
|
+
) {
|
|
347
|
+
length++;
|
|
348
|
+
}
|
|
349
|
+
best = Math.max(best, length);
|
|
350
|
+
}
|
|
351
|
+
runs.push({start: i, length: best});
|
|
352
|
+
i += best;
|
|
353
|
+
}
|
|
354
|
+
return runs.sort((a, b) => b.length - a.length);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @typedef {{name: string, title?: string, file?: string, doc?: unknown, words: {word: string, section: string | null, block: number}[], replaces: string | null, extendsTopic?: string | null}} GuideWords
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Each run of {@link PASSAGE_WORDS} words inside one string of a doc, joined.
|
|
363
|
+
* @param {{word: string, block: number}[]} words
|
|
364
|
+
* @returns {Set<string>}
|
|
365
|
+
*/
|
|
366
|
+
function windows(words) {
|
|
367
|
+
/** @type {Set<string>} */
|
|
368
|
+
const keys = new Set();
|
|
369
|
+
for (let i = 0; i + PASSAGE_WORDS <= words.length; i++) {
|
|
370
|
+
if (!inOneString(words, i, PASSAGE_WORDS)) continue;
|
|
371
|
+
keys.add(
|
|
372
|
+
words
|
|
373
|
+
.slice(i, i + PASSAGE_WORDS)
|
|
374
|
+
.map(each => each.word)
|
|
375
|
+
.join(' '),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return keys;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Guides with an index from each run of words to the guides that have it, so
|
|
383
|
+
* a topic is compared in full only with the guides it shares a run with.
|
|
384
|
+
* @typedef {{guides: GuideWords[], index: Map<string, number[]>}} IndexedGuides
|
|
385
|
+
*/
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @param {GuideWords[]} guides
|
|
389
|
+
* @returns {IndexedGuides}
|
|
390
|
+
*/
|
|
391
|
+
function indexGuides(guides) {
|
|
392
|
+
/** @type {Map<string, number[]>} */
|
|
393
|
+
const index = new Map();
|
|
394
|
+
guides.forEach((guide, at) => {
|
|
395
|
+
for (const key of windows(guide.words)) {
|
|
396
|
+
const list = index.get(key);
|
|
397
|
+
if (list) list.push(at);
|
|
398
|
+
else index.set(key, [at]);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return {guides, index};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The indexes of the guides that share at least one run with `words`.
|
|
406
|
+
* @param {{word: string, block: number}[]} words
|
|
407
|
+
* @param {IndexedGuides} indexed
|
|
408
|
+
* @returns {number[]}
|
|
409
|
+
*/
|
|
410
|
+
function candidates(words, indexed) {
|
|
411
|
+
/** @type {Set<number>} */
|
|
412
|
+
const found = new Set();
|
|
413
|
+
for (const key of windows(words)) {
|
|
414
|
+
for (const at of indexed.index.get(key) ?? []) found.add(at);
|
|
415
|
+
}
|
|
416
|
+
return [...found].sort((a, b) => a - b);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** @type {Promise<IndexedGuides> | null} */
|
|
420
|
+
let coreGuides = null;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @param {string} file
|
|
424
|
+
* @returns {Promise<any>} the doc, or null for one that does not load (the
|
|
425
|
+
* load problem is reported elsewhere)
|
|
426
|
+
*/
|
|
427
|
+
export async function loadQuietly(file) {
|
|
428
|
+
try {
|
|
429
|
+
return await loadTopicModule(file);
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** @returns {Promise<IndexedGuides>} the CLI's own guides, read and indexed once */
|
|
436
|
+
export function loadCoreGuides() {
|
|
437
|
+
coreGuides ??= (async () => {
|
|
438
|
+
/** @type {GuideWords[]} */
|
|
439
|
+
const guides = [];
|
|
440
|
+
for (const file of builtinGuideFiles()) {
|
|
441
|
+
const doc = await loadQuietly(file);
|
|
442
|
+
if (typeof doc?.name === 'string') {
|
|
443
|
+
guides.push({name: doc.name, words: proseWords(doc), replaces: null});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return indexGuides(guides);
|
|
447
|
+
})();
|
|
448
|
+
return coreGuides;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* One entry for each pair of topics that repeat a passage word for word: two
|
|
453
|
+
* of the package's own topics, or one of them and a Core topic, except the
|
|
454
|
+
* Core topic it replaces (a replacement takes that topic's place, so it may
|
|
455
|
+
* carry its text).
|
|
456
|
+
*
|
|
457
|
+
* @param {{records: {name: string, path: string, replaces?: string | null, extendsTopic?: string | null}[], guides?: {name: string, ref?: {topicFile?: string}}[]}} discovered
|
|
458
|
+
* the package's docs, as discoverIntegrationDocs returns them
|
|
459
|
+
* @returns {Promise<import('../../api/integration/authoring-checks.type.mjs').CopiedPassage[]>}
|
|
460
|
+
*/
|
|
461
|
+
export async function findCopiedPassages(discovered) {
|
|
462
|
+
const topics = [
|
|
463
|
+
...discovered.records.map(record => ({
|
|
464
|
+
name: record.name,
|
|
465
|
+
file: record.path,
|
|
466
|
+
replaces: record.replaces ?? null,
|
|
467
|
+
extendsTopic: record.extendsTopic ?? null,
|
|
468
|
+
})),
|
|
469
|
+
...(discovered.guides ?? []).map(guide => ({
|
|
470
|
+
name: guide.name,
|
|
471
|
+
file: guide.ref?.topicFile,
|
|
472
|
+
replaces: null,
|
|
473
|
+
extendsTopic: null,
|
|
474
|
+
})),
|
|
475
|
+
];
|
|
476
|
+
/** @type {GuideWords[]} */
|
|
477
|
+
const mine = [];
|
|
478
|
+
for (const each of topics) {
|
|
479
|
+
const doc = each.file ? await loadQuietly(each.file) : null;
|
|
480
|
+
if (doc) {
|
|
481
|
+
mine.push({
|
|
482
|
+
...each,
|
|
483
|
+
title: typeof doc.title === 'string' ? doc.title : each.name,
|
|
484
|
+
doc,
|
|
485
|
+
words: proseWords(doc),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (mine.length === 0) return [];
|
|
490
|
+
const core = await loadCoreGuides();
|
|
491
|
+
const coreNames = new Set(core.guides.map(guide => guide.name.toLowerCase()));
|
|
492
|
+
const packageNames = new Set(mine.map(each => each.name.toLowerCase()));
|
|
493
|
+
/**
|
|
494
|
+
* The page a topic's sections show on: its own, or the one it extends.
|
|
495
|
+
* @param {GuideWords} each
|
|
496
|
+
*/
|
|
497
|
+
const pageOf = each => (each.extendsTopic ?? each.name).toLowerCase();
|
|
498
|
+
/** @type {import('../../api/integration/authoring-checks.type.mjs').CopiedPassage[]} */
|
|
499
|
+
const found = [];
|
|
500
|
+
/**
|
|
501
|
+
* How well a passage fits a topic: how many of its content words the
|
|
502
|
+
* topic's name, title, or the section the passage sits in also use.
|
|
503
|
+
* @param {GuideWords} topic
|
|
504
|
+
* @param {string[]} words the passage's words
|
|
505
|
+
* @param {string | null} section
|
|
506
|
+
*/
|
|
507
|
+
const fit = (topic, words, section) => {
|
|
508
|
+
// A plural matches its singular: a "menus" topic fits a passage on a menu.
|
|
509
|
+
const stem = (/** @type {string} */ word) =>
|
|
510
|
+
word.length > 3 && word.endsWith('s') ? word.slice(0, -1) : word;
|
|
511
|
+
// Function words ("the", "with") fit every passage, so only content
|
|
512
|
+
// words count, as contentWords counts them.
|
|
513
|
+
const label = new Set(
|
|
514
|
+
passageWords(
|
|
515
|
+
`${topic.name.replace(/-/g, ' ')} ${topic.title ?? ''} ${section ?? ''}`,
|
|
516
|
+
)
|
|
517
|
+
.filter(word => !FUNCTION_WORDS.has(word))
|
|
518
|
+
.map(stem),
|
|
519
|
+
);
|
|
520
|
+
return new Set(words.map(stem).filter(word => label.has(word))).size;
|
|
521
|
+
};
|
|
522
|
+
/**
|
|
523
|
+
* The file line where a passage starts, found from the prose string it is
|
|
524
|
+
* in, or null.
|
|
525
|
+
* @param {GuideWords} topic
|
|
526
|
+
* @param {number} start
|
|
527
|
+
*/
|
|
528
|
+
const lineOf = (topic, start) => {
|
|
529
|
+
if (!topic.file || !topic.doc) return null;
|
|
530
|
+
const text = proseStrings(topic.doc)[topic.words[start].block - 1]?.text;
|
|
531
|
+
if (!text) return null;
|
|
532
|
+
let source;
|
|
533
|
+
try {
|
|
534
|
+
source = fs.readFileSync(topic.file, 'utf8');
|
|
535
|
+
} catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const file = fileWords(source);
|
|
539
|
+
const words = textWords(text);
|
|
540
|
+
const index = locateWords(file, words);
|
|
541
|
+
if (index < 0) return null;
|
|
542
|
+
// The passage starts at the string's word the run's first word opens.
|
|
543
|
+
let first = start;
|
|
544
|
+
while (
|
|
545
|
+
first > 0 &&
|
|
546
|
+
topic.words[first - 1].block === topic.words[start].block
|
|
547
|
+
) {
|
|
548
|
+
first -= 1;
|
|
549
|
+
}
|
|
550
|
+
const span = wordSpans(text)[start - first];
|
|
551
|
+
return span ? lineAtOffset(file, index, words, span.start) : null;
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* A passage as written: the words of the run, from the string it is in.
|
|
555
|
+
* @param {GuideWords} topic
|
|
556
|
+
* @param {number} start
|
|
557
|
+
* @param {number} length
|
|
558
|
+
* @returns {string | null}
|
|
559
|
+
*/
|
|
560
|
+
const writtenText = (topic, start, length) => {
|
|
561
|
+
if (!topic.doc) return null;
|
|
562
|
+
const {block} = topic.words[start];
|
|
563
|
+
const text = proseStrings(topic.doc)[block - 1]?.text;
|
|
564
|
+
if (!text) return null;
|
|
565
|
+
let first = start;
|
|
566
|
+
while (first > 0 && topic.words[first - 1].block === block) first -= 1;
|
|
567
|
+
const spans = wordSpans(text);
|
|
568
|
+
const from = spans[start - first];
|
|
569
|
+
const to = spans[start - first + length - 1];
|
|
570
|
+
return from && to ? text.slice(from.start, to.end) : null;
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* One entry for a pair: the topic that should keep the text (a Core topic,
|
|
574
|
+
* or of two package topics the one the passage fits), and the exact change
|
|
575
|
+
* for the other.
|
|
576
|
+
* @param {GuideWords} first
|
|
577
|
+
* @param {GuideWords} second
|
|
578
|
+
* @param {'package' | 'core'} kind
|
|
579
|
+
*/
|
|
580
|
+
const compare = (first, second, kind) => {
|
|
581
|
+
const filtered = (
|
|
582
|
+
/** @type {GuideWords} */ a,
|
|
583
|
+
/** @type {GuideWords} */ b,
|
|
584
|
+
) =>
|
|
585
|
+
sharedPassages(a.words, b.words).flatMap(({start, length}) => {
|
|
586
|
+
// Boilerplate is judged window by window: the leading and trailing
|
|
587
|
+
// 10-word windows three or more topics share are trimmed, so a
|
|
588
|
+
// lead-in many topics open with does not hide the copy after it.
|
|
589
|
+
const run = trimBoilerplate(
|
|
590
|
+
{start, length},
|
|
591
|
+
at => topicsSharing(a.words, at) >= BOILERPLATE_TOPICS,
|
|
592
|
+
);
|
|
593
|
+
return run &&
|
|
594
|
+
contentWords(a.words, run.start, run.length) >= MIN_CONTENT_WORDS
|
|
595
|
+
? [run]
|
|
596
|
+
: [];
|
|
597
|
+
});
|
|
598
|
+
let runs = filtered(first, second);
|
|
599
|
+
if (runs.length === 0) return;
|
|
600
|
+
let topic = first;
|
|
601
|
+
let owner = second;
|
|
602
|
+
let why = 'Core keeps the text it shares';
|
|
603
|
+
if (kind === 'package') {
|
|
604
|
+
const words = first.words
|
|
605
|
+
.slice(runs[0].start, runs[0].start + runs[0].length)
|
|
606
|
+
.map(each => each.word);
|
|
607
|
+
const otherRuns = filtered(second, first);
|
|
608
|
+
const firstFit = fit(first, words, first.words[runs[0].start].section);
|
|
609
|
+
const secondFit = otherRuns.length
|
|
610
|
+
? fit(second, words, second.words[otherRuns[0].start].section)
|
|
611
|
+
: 0;
|
|
612
|
+
// The passage stays where it fits best; on a tie, in the topic whose
|
|
613
|
+
// name sorts first, so the advice is the same on every run.
|
|
614
|
+
const firstKeeps =
|
|
615
|
+
firstFit > secondFit ||
|
|
616
|
+
(firstFit === secondFit && first.name.localeCompare(second.name) < 0);
|
|
617
|
+
if (firstKeeps && otherRuns.length) {
|
|
618
|
+
topic = second;
|
|
619
|
+
owner = first;
|
|
620
|
+
runs = otherRuns;
|
|
621
|
+
}
|
|
622
|
+
why =
|
|
623
|
+
firstFit === secondFit
|
|
624
|
+
? `"${owner.name}" keeps it: neither topic's name, title or section fits it better, so the one whose name sorts first does`
|
|
625
|
+
: `"${owner.name}" keeps it: its name, title or section fits the passage`;
|
|
626
|
+
}
|
|
627
|
+
const [{start, length}] = runs;
|
|
628
|
+
const written =
|
|
629
|
+
writtenText(topic, start, length) ??
|
|
630
|
+
topic.words
|
|
631
|
+
.slice(start, start + length)
|
|
632
|
+
.map(each => each.word)
|
|
633
|
+
.join(' ');
|
|
634
|
+
const excerpt =
|
|
635
|
+
written.length <= 240
|
|
636
|
+
? written
|
|
637
|
+
: `${written.slice(0, 120)} … ${written.slice(-80)}`;
|
|
638
|
+
const section = topic.words[start].section;
|
|
639
|
+
const line = lineOf(topic, start);
|
|
640
|
+
const where = [
|
|
641
|
+
section ? `section "${section}"` : null,
|
|
642
|
+
line == null ? null : `line ${line}`,
|
|
643
|
+
]
|
|
644
|
+
.filter(Boolean)
|
|
645
|
+
.join(', ');
|
|
646
|
+
const more =
|
|
647
|
+
runs.length > 1 ? `, and ${runs.length - 1} more passage(s)` : '';
|
|
648
|
+
const named =
|
|
649
|
+
kind === 'core' ? `the Core topic "${owner.name}"` : `"${owner.name}"`;
|
|
650
|
+
// An extension has no page of its own: its sections show on the page of
|
|
651
|
+
// the topic it extends, so a link to its text names that topic. Topic
|
|
652
|
+
// names resolve without regard to case, as the docs catalog reads them.
|
|
653
|
+
const page = pageOf(owner);
|
|
654
|
+
// The page is Core's only when the owner is a Core guide, or an extension
|
|
655
|
+
// of a Core topic that no package topic of the same name replaces.
|
|
656
|
+
const corePage =
|
|
657
|
+
kind === 'core' ||
|
|
658
|
+
(owner.extendsTopic != null &&
|
|
659
|
+
coreNames.has(page) &&
|
|
660
|
+
!packageNames.has(page));
|
|
661
|
+
const link = corePage
|
|
662
|
+
? `{@link @astryxdesign/cli:generic:${page}}`
|
|
663
|
+
: `{@link generic:${page}}`;
|
|
664
|
+
// A Core topic one of the package's topics replaces opens that
|
|
665
|
+
// replacement, which does not carry the passage, so no link reaches it.
|
|
666
|
+
const replacer =
|
|
667
|
+
kind === 'core'
|
|
668
|
+
? mine.find(each => each.replaces?.toLowerCase() === page)
|
|
669
|
+
: undefined;
|
|
670
|
+
// Two topics on one page (one extends the other, or both extend the same
|
|
671
|
+
// topic) would link the page to itself: the copy goes instead.
|
|
672
|
+
const fix = replacer
|
|
673
|
+
? `"${replacer.name}" replaces the Core topic "${owner.name}", so a link to it opens "${replacer.name}", which does not carry this text: reword the passage in "${topic.name}" in your own words, or move it into "${replacer.name}".`
|
|
674
|
+
: pageOf(topic) === page
|
|
675
|
+
? `"${topic.name}" and "${owner.name}" show on the same page ("${page}"): delete the passage from "${topic.name}", or, if its section overrides one there, drop the override so the original shows.`
|
|
676
|
+
: `In "${topic.name}", replace that whole passage, from its first word to its last, code ticks included, with ${link}.`;
|
|
677
|
+
found.push({
|
|
678
|
+
topic: topic.name,
|
|
679
|
+
section,
|
|
680
|
+
line,
|
|
681
|
+
otherTopic: owner.name,
|
|
682
|
+
otherOwner: kind,
|
|
683
|
+
owner: owner.name,
|
|
684
|
+
text: written,
|
|
685
|
+
words: length,
|
|
686
|
+
passages: runs.length,
|
|
687
|
+
message:
|
|
688
|
+
`"${topic.name}"${where ? ` (${where})` : ''} repeats ${length} words of ${named} word for word${more}: "${excerpt}". ` +
|
|
689
|
+
`${why}. ${fix}`,
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
const own = indexGuides(mine);
|
|
693
|
+
/**
|
|
694
|
+
* How many topics, the package's and Core's, have the run that starts here.
|
|
695
|
+
* @param {{word: string}[]} words
|
|
696
|
+
* @param {number} start
|
|
697
|
+
*/
|
|
698
|
+
const topicsSharing = (words, start) => {
|
|
699
|
+
const key = words
|
|
700
|
+
.slice(start, start + PASSAGE_WORDS)
|
|
701
|
+
.map(each => each.word)
|
|
702
|
+
.join(' ');
|
|
703
|
+
return (
|
|
704
|
+
new Set(own.index.get(key) ?? []).size +
|
|
705
|
+
new Set(core.index.get(key) ?? []).size
|
|
706
|
+
);
|
|
707
|
+
};
|
|
708
|
+
mine.forEach((topic, i) => {
|
|
709
|
+
for (const at of candidates(topic.words, own)) {
|
|
710
|
+
if (at > i) compare(topic, mine[at], 'package');
|
|
711
|
+
}
|
|
712
|
+
for (const at of candidates(topic.words, core)) {
|
|
713
|
+
const other = core.guides[at];
|
|
714
|
+
if (topic.replaces?.toLowerCase() !== other.name.toLowerCase()) {
|
|
715
|
+
compare(topic, other, 'core');
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
return found;
|
|
720
|
+
}
|