@canonical/pragma-cli 0.27.1-experimental.0 → 0.30.0
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 +67 -15
- package/dist/pragma +0 -0
- package/package.json +16 -17
- package/src/config/defaultConfig.test.ts +33 -0
- package/src/config/defaultConfig.ts +59 -0
- package/src/config/defaults.json +17 -0
- package/src/config/findProjectConfigPath.test.ts +53 -0
- package/src/config/findProjectConfigPath.ts +36 -0
- package/src/config/index.ts +17 -2
- package/src/config/mergeConfigFileUpdate.ts +58 -0
- package/src/config/parseConfigValues.ts +215 -0
- package/src/config/readConfig.test.ts +104 -13
- package/src/config/readConfig.ts +12 -117
- package/src/config/readConfigLayers.test.ts +107 -0
- package/src/config/readConfigLayers.ts +115 -0
- package/src/config/resolveGlobalConfigPath.ts +13 -0
- package/src/config/resolveWriteConfigPath.test.ts +53 -0
- package/src/config/resolveWriteConfigPath.ts +31 -0
- package/src/config/types.ts +88 -2
- package/src/config/writeConfig.ts +25 -43
- package/src/constants.ts +12 -1
- package/src/domains/block/commands/index.ts +2 -1
- package/src/domains/block/commands/list.ts +5 -90
- package/src/domains/block/commands/lookup.ts +8 -167
- package/src/domains/block/commands/sample.ts +55 -0
- package/src/domains/block/formatters/index.ts +2 -1
- package/src/domains/block/formatters/sample.ts +21 -0
- package/src/domains/block/index.ts +3 -3
- package/src/domains/block/mcp/specs.ts +36 -168
- package/src/domains/block/operations/index.ts +2 -1
- package/src/domains/block/operations/lookup.ts +9 -0
- package/src/domains/block/operations/sample.ts +41 -0
- package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
- package/src/domains/block/orchestration/resolveBlockLookup.ts +11 -3
- package/src/domains/block/stories.test.ts +28 -0
- package/src/domains/block/stories.ts +254 -0
- package/src/domains/config/commands/channel.test.ts +14 -3
- package/src/domains/config/commands/channel.ts +11 -7
- package/src/domains/config/commands/configScope.ts +46 -0
- package/src/domains/config/commands/framework.test.ts +105 -0
- package/src/domains/config/commands/framework.ts +91 -0
- package/src/domains/config/commands/index.ts +2 -0
- package/src/domains/config/commands/show.ts +5 -45
- package/src/domains/config/commands/tier.test.ts +13 -2
- package/src/domains/config/commands/tier.ts +11 -7
- package/src/domains/config/commands/trace.test.ts +165 -0
- package/src/domains/config/commands/trace.ts +83 -0
- package/src/domains/config/formatters/channel.test.ts +20 -6
- package/src/domains/config/formatters/channel.ts +14 -11
- package/src/domains/config/formatters/framework.ts +37 -0
- package/src/domains/config/formatters/index.ts +2 -0
- package/src/domains/config/formatters/show.test.ts +27 -0
- package/src/domains/config/formatters/show.ts +27 -4
- package/src/domains/config/formatters/tier.test.ts +26 -10
- package/src/domains/config/formatters/tier.ts +19 -11
- package/src/domains/config/formatters/trace.ts +28 -0
- package/src/domains/config/index.ts +15 -2
- package/src/domains/config/mcp/specs.ts +33 -53
- package/src/domains/config/operations/index.ts +1 -0
- package/src/domains/config/operations/show.test.ts +18 -0
- package/src/domains/config/operations/show.ts +7 -1
- package/src/domains/config/operations/types.ts +7 -0
- package/src/domains/config/operations/validateFramework.ts +24 -0
- package/src/domains/config/stories.ts +42 -0
- package/src/domains/config/tasks/index.ts +3 -0
- package/src/domains/config/tasks/setChannel.ts +13 -22
- package/src/domains/config/tasks/setFramework.ts +21 -0
- package/src/domains/config/tasks/setTier.ts +10 -23
- package/src/domains/config/tasks/setTrace.ts +20 -0
- package/src/domains/config/tasks/writeConfigField.ts +41 -0
- package/src/domains/create/commands/component.ts +5 -5
- package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
- package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
- package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -48
- package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
- package/src/domains/doctor/operations/checks/index.ts +1 -0
- package/src/domains/doctor/operations/checks.test.ts +162 -8
- package/src/domains/doctor/operations/runChecks.test.ts +85 -4
- package/src/domains/doctor/operations/runChecks.ts +50 -12
- package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
- package/src/domains/graph/helpers/resolveUri.ts +28 -21
- package/src/domains/graphql/commands/build.test.ts +196 -0
- package/src/domains/graphql/commands/build.ts +117 -0
- package/src/domains/graphql/commands/check.test.ts +141 -0
- package/src/domains/graphql/commands/check.ts +81 -0
- package/src/domains/graphql/commands/index.ts +5 -0
- package/src/domains/graphql/commands/serve.test.ts +40 -0
- package/src/domains/graphql/commands/serve.ts +131 -0
- package/src/domains/graphql/constants.ts +10 -0
- package/src/domains/graphql/formatters/index.ts +3 -0
- package/src/domains/graphql/formatters/report.ts +78 -0
- package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
- package/src/domains/graphql/helpers/gatherSources.ts +58 -0
- package/src/domains/graphql/helpers/index.ts +6 -0
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
- package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
- package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
- package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
- package/src/domains/graphql/index.ts +24 -0
- package/src/domains/graphql/operations/compileSchema.test.ts +95 -0
- package/src/domains/graphql/operations/compileSchema.ts +107 -0
- package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
- package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
- package/src/domains/graphql/operations/index.ts +12 -0
- package/src/domains/graphql/types.ts +29 -0
- package/src/domains/info/operations/collectInfo.test.ts +16 -5
- package/src/domains/info/operations/collectInfo.ts +16 -19
- package/src/domains/llm/capabilities.test.ts +91 -0
- package/src/domains/llm/commands/capabilities.ts +28 -0
- package/src/domains/llm/commands/index.ts +1 -0
- package/src/domains/llm/data/commandReference.ts +90 -18
- package/src/domains/llm/data/decisionTrees.ts +15 -13
- package/src/domains/llm/data/index.ts +2 -0
- package/src/domains/llm/data/toolCatalog.ts +190 -0
- package/src/domains/llm/formatters/capabilities.ts +50 -0
- package/src/domains/llm/formatters/index.ts +1 -0
- package/src/domains/llm/index.ts +3 -2
- package/src/domains/llm/mcp/index.ts +1 -1
- package/src/domains/llm/mcp/specs.ts +59 -52
- package/src/domains/llm/types.ts +40 -0
- package/src/domains/modifier/commands/index.ts +2 -1
- package/src/domains/modifier/commands/list.ts +5 -38
- package/src/domains/modifier/commands/lookup.ts +8 -79
- package/src/domains/modifier/commands/sample.ts +53 -0
- package/src/domains/modifier/formatters/index.ts +2 -1
- package/src/domains/modifier/formatters/sample.ts +15 -0
- package/src/domains/modifier/index.ts +7 -3
- package/src/domains/modifier/mcp/specs.ts +38 -70
- package/src/domains/modifier/operations/index.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +9 -0
- package/src/domains/modifier/operations/sample.ts +32 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +15 -2
- package/src/domains/modifier/stories.test.ts +20 -0
- package/src/domains/modifier/stories.ts +77 -0
- package/src/domains/ontology/commands/list.ts +5 -58
- package/src/domains/ontology/commands/show.ts +6 -58
- package/src/domains/ontology/formatters/show.ts +7 -1
- package/src/domains/ontology/mcp/specs.ts +7 -84
- package/src/domains/ontology/stories.test.ts +36 -0
- package/src/domains/ontology/stories.ts +108 -0
- package/src/domains/refs/operations/index.ts +1 -1
- package/src/domains/refs/operations/paths.ts +29 -1
- package/src/domains/refs/operations/updateRefs.test.ts +6 -3
- package/src/domains/refs/operations/updateRefs.ts +2 -36
- package/src/domains/setup/commands/skills.test.ts +6 -2
- package/src/domains/setup/commands/skills.ts +6 -2
- package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
- package/src/domains/setup/helpers/runSetupTask.ts +62 -70
- package/src/domains/setup/operations/setupMcp.test.ts +6 -1
- package/src/domains/shared/bootStore.ts +92 -50
- package/src/domains/shared/compactUri.test.ts +11 -0
- package/src/domains/shared/compactUri.ts +10 -5
- package/src/domains/shared/context.ts +2 -2
- package/src/domains/shared/contracts.ts +12 -0
- package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
- package/src/domains/shared/createInteractivePromptSession.ts +236 -0
- package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
- package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
- package/src/domains/shared/loaders/bundledLoader.ts +206 -0
- package/src/domains/shared/loaders/gitLoader.ts +31 -0
- package/src/domains/shared/loaders/index.ts +9 -0
- package/src/domains/shared/loaders/localLoader.ts +87 -0
- package/src/domains/shared/loaders/readPackageDir.ts +147 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
- package/src/domains/shared/lookupMany.test.ts +120 -0
- package/src/domains/shared/lookupMany.ts +43 -3
- package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
- package/src/domains/shared/mergeAndParseRefs.ts +44 -0
- package/src/domains/shared/packages.ts +14 -120
- package/src/domains/shared/pickRandom.ts +22 -0
- package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
- package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
- package/src/domains/shared/runtime.ts +7 -63
- package/src/domains/shared/sampleFormatters.ts +47 -0
- package/src/domains/shared/semanticPackage.ts +112 -0
- package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
- package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
- package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
- package/src/domains/shared/stories/compileLookupTool.ts +96 -0
- package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
- package/src/domains/shared/stories/compileReadCommand.ts +48 -0
- package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
- package/src/domains/shared/stories/compileReadTool.ts +38 -0
- package/src/domains/shared/stories/condense.test.ts +18 -0
- package/src/domains/shared/stories/condense.ts +15 -0
- package/src/domains/shared/stories/index.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.ts +22 -0
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
- package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
- package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
- package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
- package/src/domains/shared/stories/pack/index.ts +28 -0
- package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
- package/src/domains/shared/stories/pack/types.ts +104 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
- package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
- package/src/domains/shared/stories/projectCliParameters.ts +57 -0
- package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
- package/src/domains/shared/stories/projectToolParams.ts +32 -0
- package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
- package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
- package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
- package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
- package/src/domains/shared/stories/types.ts +171 -0
- package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
- package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
- package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
- package/src/domains/shared/suggestions/expandGlob.ts +52 -0
- package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
- package/src/domains/shared/suggestions/index.ts +7 -0
- package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
- package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
- package/src/domains/shared/suggestions/suggestNames.ts +130 -0
- package/src/domains/shared/types/index.ts +1 -0
- package/src/domains/shared/types/runtime.ts +4 -1
- package/src/domains/shared/types/sample.ts +17 -0
- package/src/domains/skill/commands/index.ts +3 -1
- package/src/domains/skill/commands/list.test.ts +6 -2
- package/src/domains/skill/commands/list.ts +5 -63
- package/src/domains/skill/commands/lookup.test.ts +118 -0
- package/src/domains/skill/commands/lookup.ts +14 -0
- package/src/domains/skill/formatters/index.ts +1 -0
- package/src/domains/skill/formatters/lookup.test.ts +46 -0
- package/src/domains/skill/formatters/lookup.ts +57 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +48 -16
- package/src/domains/skill/index.ts +7 -3
- package/src/domains/skill/mcp/specs.ts +10 -29
- package/src/domains/skill/operations/discover.test.ts +61 -30
- package/src/domains/skill/operations/discover.ts +28 -37
- package/src/domains/skill/operations/index.ts +2 -1
- package/src/domains/skill/operations/list.test.ts +15 -7
- package/src/domains/skill/operations/lookup.test.ts +91 -0
- package/src/domains/skill/operations/lookup.ts +70 -0
- package/src/domains/skill/stories.test.ts +43 -0
- package/src/domains/skill/stories.ts +103 -0
- package/src/domains/skill/types.ts +8 -0
- package/src/domains/standard/commands/index.ts +2 -1
- package/src/domains/standard/commands/list.ts +5 -84
- package/src/domains/standard/commands/lookup.ts +8 -93
- package/src/domains/standard/commands/sample.ts +57 -0
- package/src/domains/standard/formatters/index.ts +2 -1
- package/src/domains/standard/formatters/sample.ts +15 -0
- package/src/domains/standard/index.ts +8 -1
- package/src/domains/standard/mcp/specs.ts +44 -152
- package/src/domains/standard/operations/index.ts +2 -1
- package/src/domains/standard/operations/lookup.ts +9 -0
- package/src/domains/standard/operations/sample.ts +35 -0
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +15 -2
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
- package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
- package/src/domains/standard/stories.test.ts +24 -0
- package/src/domains/standard/stories.ts +151 -0
- package/src/domains/tier/commands/list.ts +5 -55
- package/src/domains/tier/mcp/specs.ts +5 -31
- package/src/domains/tier/stories.test.ts +22 -0
- package/src/domains/tier/stories.ts +52 -0
- package/src/domains/token/commands/index.ts +2 -1
- package/src/domains/token/commands/sample.ts +53 -0
- package/src/domains/token/formatters/index.ts +2 -1
- package/src/domains/token/formatters/sample.ts +16 -0
- package/src/domains/token/index.ts +8 -1
- package/src/domains/token/mcp/specs.ts +52 -2
- package/src/domains/token/operations/index.ts +2 -1
- package/src/domains/token/operations/lookup.ts +9 -0
- package/src/domains/token/operations/sample.ts +32 -0
- package/src/domains/token/orchestration/resolveTokenLookup.ts +15 -2
- package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
- package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
- package/src/domains/trace/commands/clear.ts +71 -0
- package/src/domains/trace/commands/index.ts +5 -0
- package/src/domains/trace/commands/sessions.ts +32 -0
- package/src/domains/trace/commands/show.ts +145 -0
- package/src/domains/trace/formatters/index.ts +9 -0
- package/src/domains/trace/formatters/sessions.ts +80 -0
- package/src/domains/trace/formatters/show.ts +122 -0
- package/src/domains/trace/index.ts +17 -0
- package/src/domains/trace/operations/index.ts +8 -0
- package/src/domains/trace/operations/listSessions.test.ts +55 -0
- package/src/domains/trace/operations/listSessions.ts +52 -0
- package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
- package/src/domains/trace/operations/readTraceLog.ts +104 -0
- package/src/domains/trace/tracePlugin.test.ts +171 -0
- package/src/domains/trace/tracePlugin.ts +159 -0
- package/src/domains/trace/types.ts +65 -0
- package/src/error/PragmaError.ts +15 -2
- package/src/error/types.ts +15 -1
- package/src/mcp/error/serializeErrorPayload.ts +7 -0
- package/src/mcp/error/types.ts +8 -0
- package/src/mcp/registerResources.test.ts +405 -75
- package/src/mcp/registerResources.ts +5 -299
- package/src/mcp/resources/buildGraphIndex.ts +167 -0
- package/src/mcp/resources/buildResourceList.test.ts +97 -0
- package/src/mcp/resources/buildResourceList.ts +176 -0
- package/src/mcp/resources/classifyEntity.test.ts +40 -0
- package/src/mcp/resources/classifyEntity.ts +27 -0
- package/src/mcp/resources/constants.ts +105 -0
- package/src/mcp/resources/displayPredicates.ts +62 -0
- package/src/mcp/resources/pickFirstValue.test.ts +29 -0
- package/src/mcp/resources/pickFirstValue.ts +26 -0
- package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
- package/src/mcp/resources/pickPrimaryType.ts +26 -0
- package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
- package/src/mcp/resources/rankUriCompletions.ts +64 -0
- package/src/mcp/resources/readEntity.ts +169 -0
- package/src/mcp/resources/registerResources.ts +126 -0
- package/src/mcp/resources/types.ts +128 -0
- package/src/mcp/tools/index.ts +9 -0
- package/src/mcp/tools/registerTools.test.ts +84 -12
- package/src/mcp/types.ts +4 -0
- package/src/mcp/utils/wrapTool.test.ts +1 -0
- package/src/pipeline/collectCommands.ts +15 -2
- package/src/pipeline/renderError.ts +15 -0
- package/src/pipeline/resolveCommandKind.ts +8 -1
- package/src/pipeline/runCli.ts +51 -6
- package/src/testing/createTestMcpClient.ts +5 -1
- package/src/testing/graphqlFixtures.ts +72 -0
- package/src/testing/index.ts +17 -0
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
- package/src/testing/integration/executor-execution.test.ts +101 -0
- package/src/testing/integration/executor-golden.test.ts +145 -0
- package/src/testing/integration/mcp-surface.test.ts +11 -5
- package/src/testing/integration/parity.test.ts +17 -12
- package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
- package/src/testing/setupXdgIsolation.ts +15 -0
- package/src/testing/standardParityFixtures.ts +92 -0
- package/src/testing/storyFixtures.ts +63 -0
- package/src/pipeline/runInteractiveCommand.ts +0 -198
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import extractLocalName from "../../domains/shared/extractLocalName.js";
|
|
2
|
+
import {
|
|
3
|
+
ABOX_PER_CLASS_LIMIT,
|
|
4
|
+
ABOX_TOTAL_LIMIT,
|
|
5
|
+
CLASS_PRIORITY,
|
|
6
|
+
INDIVIDUAL_PRIORITY,
|
|
7
|
+
NAME_SEPARATOR,
|
|
8
|
+
PROPERTY_PRIORITY,
|
|
9
|
+
RESOURCE_META_KEYS,
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
import type {
|
|
12
|
+
GraphEntity,
|
|
13
|
+
GraphIndex,
|
|
14
|
+
ListedResource,
|
|
15
|
+
ResourceListing,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
/** Longest description surfaced in a listing entry before truncation. */
|
|
19
|
+
const DESCRIPTION_MAX = 120;
|
|
20
|
+
|
|
21
|
+
/** Human-facing display name for an entity: its label, else its local name. */
|
|
22
|
+
function toDisplayName(entity: GraphEntity): string {
|
|
23
|
+
return entity.label ?? extractLocalName(entity.uri);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Truncate a description to a listing-friendly length. */
|
|
27
|
+
function toShortDescription(text: string): string {
|
|
28
|
+
return text.length > DESCRIPTION_MAX
|
|
29
|
+
? `${text.slice(0, DESCRIPTION_MAX - 3)}...`
|
|
30
|
+
: text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Sort entities by display name for stable, legible ordering. */
|
|
34
|
+
function byDisplayName(a: GraphEntity, b: GraphEntity): number {
|
|
35
|
+
return toDisplayName(a).localeCompare(toDisplayName(b));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build the listing entry for a TBox class, carrying its individual counts. */
|
|
39
|
+
function toClassResource(
|
|
40
|
+
entity: GraphEntity,
|
|
41
|
+
instanceCount: number,
|
|
42
|
+
instancesShown: number,
|
|
43
|
+
): ListedResource {
|
|
44
|
+
const truncated = instancesShown < instanceCount;
|
|
45
|
+
const description = truncated
|
|
46
|
+
? `Schema class · showing ${instancesShown} of ${instanceCount} individuals; use autocomplete or graph_query for the rest`
|
|
47
|
+
: `Schema class · ${instanceCount} individual${instanceCount === 1 ? "" : "s"}`;
|
|
48
|
+
return {
|
|
49
|
+
uri: entity.prefixed,
|
|
50
|
+
name: `Class${NAME_SEPARATOR}${toDisplayName(entity)}`,
|
|
51
|
+
description,
|
|
52
|
+
mimeType: "application/json",
|
|
53
|
+
annotations: { audience: ["assistant"], priority: CLASS_PRIORITY },
|
|
54
|
+
_meta: {
|
|
55
|
+
[RESOURCE_META_KEYS.box]: "tbox",
|
|
56
|
+
[RESOURCE_META_KEYS.category]: "class",
|
|
57
|
+
[RESOURCE_META_KEYS.instanceCount]: instanceCount,
|
|
58
|
+
[RESOURCE_META_KEYS.instancesShown]: instancesShown,
|
|
59
|
+
[RESOURCE_META_KEYS.truncated]: truncated,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Build the listing entry for a TBox property. */
|
|
65
|
+
function toPropertyResource(entity: GraphEntity): ListedResource {
|
|
66
|
+
const construct = entity.types.find((t) => t.endsWith("Property"));
|
|
67
|
+
const description = construct
|
|
68
|
+
? `Schema property · ${construct}`
|
|
69
|
+
: "Schema property";
|
|
70
|
+
return {
|
|
71
|
+
uri: entity.prefixed,
|
|
72
|
+
name: `Property${NAME_SEPARATOR}${toDisplayName(entity)}`,
|
|
73
|
+
description,
|
|
74
|
+
mimeType: "application/json",
|
|
75
|
+
annotations: { audience: ["assistant"], priority: PROPERTY_PRIORITY },
|
|
76
|
+
_meta: {
|
|
77
|
+
[RESOURCE_META_KEYS.box]: "tbox",
|
|
78
|
+
[RESOURCE_META_KEYS.category]: "property",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Build the listing entry for an ABox individual, grouped under its class. */
|
|
84
|
+
function toIndividualResource(entity: GraphEntity): ListedResource {
|
|
85
|
+
const group = entity.primaryTypeLabel ?? "Individual";
|
|
86
|
+
const summary = entity.description
|
|
87
|
+
? toShortDescription(entity.description)
|
|
88
|
+
: null;
|
|
89
|
+
const description = summary ? `${group}${NAME_SEPARATOR}${summary}` : group;
|
|
90
|
+
return {
|
|
91
|
+
uri: entity.prefixed,
|
|
92
|
+
name: `${group}${NAME_SEPARATOR}${toDisplayName(entity)}`,
|
|
93
|
+
description,
|
|
94
|
+
mimeType: "application/json",
|
|
95
|
+
annotations: { audience: ["assistant"], priority: INDIVIDUAL_PRIORITY },
|
|
96
|
+
_meta: {
|
|
97
|
+
[RESOURCE_META_KEYS.box]: "abox",
|
|
98
|
+
[RESOURCE_META_KEYS.category]: "individual",
|
|
99
|
+
[RESOURCE_META_KEYS.type]: entity.primaryType ?? null,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the ordered, categorized, capped MCP resource listing from the index.
|
|
106
|
+
*
|
|
107
|
+
* TBox schema comes first and in full (classes, then properties) so a consumer
|
|
108
|
+
* sees the schema without scrolling past every instance. ABox individuals
|
|
109
|
+
* follow, grouped by class and sorted, capped per class ({@link
|
|
110
|
+
* ABOX_PER_CLASS_LIMIT}) and overall ({@link ABOX_TOTAL_LIMIT}). Dropped
|
|
111
|
+
* individuals are reported in the returned {@link ResourceListing.truncation}
|
|
112
|
+
* and reflected in each class entry's count metadata — never silently lost.
|
|
113
|
+
*
|
|
114
|
+
* @param index - The graph index of classified subjects.
|
|
115
|
+
* @returns The resource entries and a summary of any capping.
|
|
116
|
+
*/
|
|
117
|
+
export default function buildResourceList(index: GraphIndex): ResourceListing {
|
|
118
|
+
const classes = index.entities
|
|
119
|
+
.filter((e) => e.category === "class")
|
|
120
|
+
.sort(byDisplayName);
|
|
121
|
+
const properties = index.entities
|
|
122
|
+
.filter((e) => e.category === "property")
|
|
123
|
+
.sort(byDisplayName);
|
|
124
|
+
const individuals = index.entities.filter((e) => e.category === "individual");
|
|
125
|
+
|
|
126
|
+
const groups = new Map<string, GraphEntity[]>();
|
|
127
|
+
for (const individual of individuals) {
|
|
128
|
+
const key = individual.primaryType ?? "";
|
|
129
|
+
const bucket = groups.get(key) ?? [];
|
|
130
|
+
bucket.push(individual);
|
|
131
|
+
groups.set(key, bucket);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const orderedKeys = [...groups.keys()].sort((a, b) => a.localeCompare(b));
|
|
135
|
+
const shownByType = new Map<string, number>();
|
|
136
|
+
const droppedByType = new Map<string, number>();
|
|
137
|
+
const individualResources: ListedResource[] = [];
|
|
138
|
+
let totalShown = 0;
|
|
139
|
+
|
|
140
|
+
for (const key of orderedKeys) {
|
|
141
|
+
const bucket = (groups.get(key) ?? []).sort(byDisplayName);
|
|
142
|
+
let groupShown = 0;
|
|
143
|
+
for (const individual of bucket) {
|
|
144
|
+
const atGlobalCap = totalShown >= ABOX_TOTAL_LIMIT;
|
|
145
|
+
const atClassCap = groupShown >= ABOX_PER_CLASS_LIMIT;
|
|
146
|
+
if (atGlobalCap || atClassCap) {
|
|
147
|
+
droppedByType.set(key, (droppedByType.get(key) ?? 0) + 1);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
individualResources.push(toIndividualResource(individual));
|
|
151
|
+
groupShown += 1;
|
|
152
|
+
totalShown += 1;
|
|
153
|
+
}
|
|
154
|
+
shownByType.set(key, groupShown);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const classResources = classes.map((entity) =>
|
|
158
|
+
toClassResource(
|
|
159
|
+
entity,
|
|
160
|
+
(shownByType.get(entity.prefixed) ?? 0) +
|
|
161
|
+
(droppedByType.get(entity.prefixed) ?? 0),
|
|
162
|
+
shownByType.get(entity.prefixed) ?? 0,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const totalDropped = [...droppedByType.values()].reduce((a, b) => a + b, 0);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
resources: [
|
|
170
|
+
...classResources,
|
|
171
|
+
...properties.map(toPropertyResource),
|
|
172
|
+
...individualResources,
|
|
173
|
+
],
|
|
174
|
+
truncation: { totalDropped, droppedByType },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import classifyEntity from "./classifyEntity.js";
|
|
3
|
+
|
|
4
|
+
const OWL_CLASS = "http://www.w3.org/2002/07/owl#Class";
|
|
5
|
+
const OWL_OBJECT_PROPERTY = "http://www.w3.org/2002/07/owl#ObjectProperty";
|
|
6
|
+
const DS_COMPONENT = "https://ds.canonical.com/Component";
|
|
7
|
+
|
|
8
|
+
describe("classifyEntity", () => {
|
|
9
|
+
it("classifies owl:Class as tbox/class", () => {
|
|
10
|
+
expect(classifyEntity([OWL_CLASS])).toEqual({
|
|
11
|
+
box: "tbox",
|
|
12
|
+
category: "class",
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("classifies a property construct as tbox/property", () => {
|
|
17
|
+
expect(classifyEntity([OWL_OBJECT_PROPERTY])).toEqual({
|
|
18
|
+
box: "tbox",
|
|
19
|
+
category: "property",
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("classifies a domain-typed subject as abox/individual", () => {
|
|
24
|
+
expect(classifyEntity([DS_COMPONENT])).toEqual({
|
|
25
|
+
box: "abox",
|
|
26
|
+
category: "individual",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("prefers schema when a subject is punned as both class and instance", () => {
|
|
31
|
+
expect(classifyEntity([DS_COMPONENT, OWL_CLASS])).toEqual({
|
|
32
|
+
box: "tbox",
|
|
33
|
+
category: "class",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns null for an untyped subject", () => {
|
|
38
|
+
expect(classifyEntity([])).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CLASS_TYPE_URIS, PROPERTY_TYPE_URIS } from "./constants.js";
|
|
2
|
+
import type { EntityBox, EntityCategory } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Classify a subject into its box and category from its `rdf:type` values.
|
|
6
|
+
*
|
|
7
|
+
* Schema constructs win over instance-hood: a subject typed as both a class
|
|
8
|
+
* and something else is a class, so the schema is never buried among
|
|
9
|
+
* individuals. A subject with any non-schema type is an individual; a subject
|
|
10
|
+
* with no type at all returns `null` (the caller decides how to surface it).
|
|
11
|
+
*
|
|
12
|
+
* @param typeUris - Full `rdf:type` object URIs asserted for the subject.
|
|
13
|
+
* @returns The box and category, or `null` when the subject has no type.
|
|
14
|
+
*/
|
|
15
|
+
export default function classifyEntity(
|
|
16
|
+
typeUris: readonly string[],
|
|
17
|
+
): { box: EntityBox; category: EntityCategory } | null {
|
|
18
|
+
const hasClassType = typeUris.some((t) => CLASS_TYPE_URIS.includes(t));
|
|
19
|
+
if (hasClassType) return { box: "tbox", category: "class" };
|
|
20
|
+
|
|
21
|
+
const hasPropertyType = typeUris.some((t) => PROPERTY_TYPE_URIS.includes(t));
|
|
22
|
+
if (hasPropertyType) return { box: "tbox", category: "property" };
|
|
23
|
+
|
|
24
|
+
if (typeUris.length > 0) return { box: "abox", category: "individual" };
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for the MCP resource surface.
|
|
3
|
+
*
|
|
4
|
+
* Collects the RDF/RDFS/OWL term URIs, the label/description fallback
|
|
5
|
+
* chains that let foreign ontologies (no `PROPERTY_MAP` entry) still
|
|
6
|
+
* resolve human names, the listing/completion caps, and the client-facing
|
|
7
|
+
* annotation priorities and `_meta` keys used to distinguish TBox schema
|
|
8
|
+
* from ABox individuals.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PREFIX_MAP } from "../../domains/shared/prefixes.js";
|
|
12
|
+
|
|
13
|
+
const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
|
|
14
|
+
const DCTERMS_NS = "http://purl.org/dc/terms/";
|
|
15
|
+
const SCHEMA_NS = "http://schema.org/";
|
|
16
|
+
|
|
17
|
+
/** `rdf:type` predicate URI. */
|
|
18
|
+
export const RDF_TYPE = `${RDF_NS}type`;
|
|
19
|
+
/** `rdfs:subClassOf` predicate URI. */
|
|
20
|
+
export const RDFS_SUBCLASS_OF = `${PREFIX_MAP.rdfs}subClassOf`;
|
|
21
|
+
/** `rdfs:domain` predicate URI. */
|
|
22
|
+
export const RDFS_DOMAIN = `${PREFIX_MAP.rdfs}domain`;
|
|
23
|
+
/** `rdfs:range` predicate URI. */
|
|
24
|
+
export const RDFS_RANGE = `${PREFIX_MAP.rdfs}range`;
|
|
25
|
+
|
|
26
|
+
/** Class-declaring `rdf:type` objects (TBox class constructs). */
|
|
27
|
+
export const CLASS_TYPE_URIS: readonly string[] = [
|
|
28
|
+
`${PREFIX_MAP.owl}Class`,
|
|
29
|
+
`${PREFIX_MAP.rdfs}Class`,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/** Property-declaring `rdf:type` objects (TBox property constructs). */
|
|
33
|
+
export const PROPERTY_TYPE_URIS: readonly string[] = [
|
|
34
|
+
`${PREFIX_MAP.owl}ObjectProperty`,
|
|
35
|
+
`${PREFIX_MAP.owl}DatatypeProperty`,
|
|
36
|
+
`${PREFIX_MAP.owl}AnnotationProperty`,
|
|
37
|
+
`${RDF_NS}Property`,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `rdf:type` objects that never make a meaningful grouping or `instanceOf`
|
|
42
|
+
* pointer for an individual: the schema-construct meta-types plus the
|
|
43
|
+
* top/individual meta-types (`owl:NamedIndividual`, `owl:Thing`,
|
|
44
|
+
* `rdfs:Resource`). An individual typed `[ds:Component, owl:NamedIndividual]`
|
|
45
|
+
* must group under `ds:Component`, so these are filtered out before a
|
|
46
|
+
* primary type is chosen.
|
|
47
|
+
*/
|
|
48
|
+
export const NON_GROUPING_TYPE_URIS: readonly string[] = [
|
|
49
|
+
...CLASS_TYPE_URIS,
|
|
50
|
+
...PROPERTY_TYPE_URIS,
|
|
51
|
+
`${PREFIX_MAP.owl}NamedIndividual`,
|
|
52
|
+
`${PREFIX_MAP.owl}Thing`,
|
|
53
|
+
`${PREFIX_MAP.rdfs}Resource`,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generic label predicates tried after a namespace's `PROPERTY_MAP` label,
|
|
58
|
+
* in priority order. Lets foreign ontologies resolve a name.
|
|
59
|
+
*/
|
|
60
|
+
export const LABEL_FALLBACK_URIS: readonly string[] = [
|
|
61
|
+
`${PREFIX_MAP.rdfs}label`,
|
|
62
|
+
`${PREFIX_MAP.skos}prefLabel`,
|
|
63
|
+
`${DCTERMS_NS}title`,
|
|
64
|
+
`${SCHEMA_NS}name`,
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generic description predicates tried after a namespace's `PROPERTY_MAP`
|
|
69
|
+
* description, in priority order.
|
|
70
|
+
*/
|
|
71
|
+
export const DESCRIPTION_FALLBACK_URIS: readonly string[] = [
|
|
72
|
+
`${PREFIX_MAP.rdfs}comment`,
|
|
73
|
+
`${DCTERMS_NS}description`,
|
|
74
|
+
`${SCHEMA_NS}description`,
|
|
75
|
+
`${PREFIX_MAP.skos}definition`,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** Maximum individuals listed per class before the remainder is capped. */
|
|
79
|
+
export const ABOX_PER_CLASS_LIMIT = 25;
|
|
80
|
+
|
|
81
|
+
/** Hard ceiling on total individuals across all classes in one listing. */
|
|
82
|
+
export const ABOX_TOTAL_LIMIT = 300;
|
|
83
|
+
|
|
84
|
+
/** Maximum ranked suggestions returned from URI autocomplete. */
|
|
85
|
+
export const COMPLETION_LIMIT = 50;
|
|
86
|
+
|
|
87
|
+
/** Annotation `priority` for TBox classes (highest — schema first). */
|
|
88
|
+
export const CLASS_PRIORITY = 1;
|
|
89
|
+
/** Annotation `priority` for TBox properties. */
|
|
90
|
+
export const PROPERTY_PRIORITY = 0.9;
|
|
91
|
+
/** Annotation `priority` for ABox individuals (lowest). */
|
|
92
|
+
export const INDIVIDUAL_PRIORITY = 0.5;
|
|
93
|
+
|
|
94
|
+
/** Separator between the category tag and the entity label in resource names. */
|
|
95
|
+
export const NAME_SEPARATOR = " · ";
|
|
96
|
+
|
|
97
|
+
/** Namespaced `_meta` keys carrying structured box/category metadata. */
|
|
98
|
+
export const RESOURCE_META_KEYS = {
|
|
99
|
+
box: "pragma/box",
|
|
100
|
+
category: "pragma/category",
|
|
101
|
+
type: "pragma/type",
|
|
102
|
+
instanceCount: "pragma/instanceCount",
|
|
103
|
+
instancesShown: "pragma/instancesShown",
|
|
104
|
+
truncated: "pragma/truncated",
|
|
105
|
+
} as const;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label and description predicate chains for display-value resolution.
|
|
3
|
+
*
|
|
4
|
+
* Two concerns share this file because they are one policy: how a subject's
|
|
5
|
+
* human name and blurb are found. `resolve*` builds the per-subject priority
|
|
6
|
+
* order (namespace-specific `PROPERTY_MAP` predicate first, then the generic
|
|
7
|
+
* fallback chain); `collect*` builds the flat union used to bulk-query every
|
|
8
|
+
* candidate predicate at once. Keeping them together keeps the fallback
|
|
9
|
+
* ordering and the bulk query in lockstep.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { PROPERTY_MAP } from "../../constants.js";
|
|
13
|
+
import { DESCRIPTION_FALLBACK_URIS, LABEL_FALLBACK_URIS } from "./constants.js";
|
|
14
|
+
|
|
15
|
+
/** Narrow away `undefined` while preserving the string type. */
|
|
16
|
+
function isPresent(value: string | undefined): value is string {
|
|
17
|
+
return value !== undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ordered label predicates for a subject: its namespace's `PROPERTY_MAP`
|
|
22
|
+
* label first, then the generic chain so foreign namespaces still resolve.
|
|
23
|
+
*
|
|
24
|
+
* @param prefix - The subject's namespace prefix, or `undefined` if foreign.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveLabelPredicates(prefix: string | undefined): string[] {
|
|
27
|
+
const specific = prefix ? PROPERTY_MAP[prefix]?.label : undefined;
|
|
28
|
+
return [...new Set([specific, ...LABEL_FALLBACK_URIS].filter(isPresent))];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ordered description predicates for a subject: its namespace's `PROPERTY_MAP`
|
|
33
|
+
* description then definition, then the generic chain.
|
|
34
|
+
*
|
|
35
|
+
* @param prefix - The subject's namespace prefix, or `undefined` if foreign.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveDescriptionPredicates(
|
|
38
|
+
prefix: string | undefined,
|
|
39
|
+
): string[] {
|
|
40
|
+
const map = prefix ? PROPERTY_MAP[prefix] : undefined;
|
|
41
|
+
return [
|
|
42
|
+
...new Set(
|
|
43
|
+
[map?.description, map?.definition, ...DESCRIPTION_FALLBACK_URIS].filter(
|
|
44
|
+
isPresent,
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Every label predicate any namespace might use, for one bulk query. */
|
|
51
|
+
export function collectLabelPredicates(): string[] {
|
|
52
|
+
const fromMap = Object.values(PROPERTY_MAP).map((m) => m.label);
|
|
53
|
+
return [...new Set([...fromMap, ...LABEL_FALLBACK_URIS])];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Every description predicate any namespace might use, for one bulk query. */
|
|
57
|
+
export function collectDescriptionPredicates(): string[] {
|
|
58
|
+
const fromMap = Object.values(PROPERTY_MAP).flatMap((m) =>
|
|
59
|
+
[m.description, m.definition].filter(isPresent),
|
|
60
|
+
);
|
|
61
|
+
return [...new Set([...fromMap, ...DESCRIPTION_FALLBACK_URIS])];
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import pickFirstValue from "./pickFirstValue.js";
|
|
3
|
+
|
|
4
|
+
describe("pickFirstValue", () => {
|
|
5
|
+
it("returns the value of the first predicate present in priority order", () => {
|
|
6
|
+
const values = new Map([
|
|
7
|
+
["p2", ["second"]],
|
|
8
|
+
["p3", ["third"]],
|
|
9
|
+
]);
|
|
10
|
+
expect(pickFirstValue(values, ["p1", "p2", "p3"])).toBe("second");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("skips empty values", () => {
|
|
14
|
+
const values = new Map([
|
|
15
|
+
["p1", [""]],
|
|
16
|
+
["p2", ["real"]],
|
|
17
|
+
]);
|
|
18
|
+
expect(pickFirstValue(values, ["p1", "p2"])).toBe("real");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns null when no predicate matches", () => {
|
|
22
|
+
const values = new Map([["other", ["x"]]]);
|
|
23
|
+
expect(pickFirstValue(values, ["p1", "p2"])).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns null for an empty map", () => {
|
|
27
|
+
expect(pickFirstValue(new Map(), ["p1"])).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the first available value from a predicate priority chain.
|
|
3
|
+
*
|
|
4
|
+
* Walks `orderedPredicates` in order and returns the first non-empty value
|
|
5
|
+
* present in `valuesByPredicate`. This drives the label/description fallback:
|
|
6
|
+
* a namespace's `PROPERTY_MAP` predicate is tried first, then the generic
|
|
7
|
+
* chain (`rdfs:label`, `skos:prefLabel`, …), so foreign ontologies still
|
|
8
|
+
* resolve a name.
|
|
9
|
+
*
|
|
10
|
+
* @param valuesByPredicate - Full predicate URI → asserted lexical values.
|
|
11
|
+
* @param orderedPredicates - Predicate URIs in descending priority.
|
|
12
|
+
* @returns The first non-empty value, or `null` when none match.
|
|
13
|
+
*/
|
|
14
|
+
export default function pickFirstValue(
|
|
15
|
+
valuesByPredicate: ReadonlyMap<string, readonly string[]>,
|
|
16
|
+
orderedPredicates: readonly string[],
|
|
17
|
+
): string | null {
|
|
18
|
+
for (const predicate of orderedPredicates) {
|
|
19
|
+
const values = valuesByPredicate.get(predicate);
|
|
20
|
+
if (values === undefined) continue;
|
|
21
|
+
for (const value of values) {
|
|
22
|
+
if (value.length > 0) return value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PREFIX_MAP } from "../../domains/shared/prefixes.js";
|
|
3
|
+
import pickPrimaryType from "./pickPrimaryType.js";
|
|
4
|
+
|
|
5
|
+
const COMPONENT = `${PREFIX_MAP.ds}Component`;
|
|
6
|
+
const BLOCK = `${PREFIX_MAP.ds}Block`;
|
|
7
|
+
const NAMED_INDIVIDUAL = `${PREFIX_MAP.owl}NamedIndividual`;
|
|
8
|
+
const OWL_THING = `${PREFIX_MAP.owl}Thing`;
|
|
9
|
+
|
|
10
|
+
describe("pickPrimaryType", () => {
|
|
11
|
+
it("returns null for no types", () => {
|
|
12
|
+
expect(pickPrimaryType([])).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("picks the domain type over a meta-type that sorts first", () => {
|
|
16
|
+
// owl: is http://… and ds: is https://…, so the meta-type sorts first;
|
|
17
|
+
// the meaningful domain type must still win.
|
|
18
|
+
expect([...[NAMED_INDIVIDUAL, COMPONENT]].sort().at(0)).toBe(
|
|
19
|
+
NAMED_INDIVIDUAL,
|
|
20
|
+
);
|
|
21
|
+
expect(pickPrimaryType([NAMED_INDIVIDUAL, COMPONENT])).toBe(COMPONENT);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("ignores owl:Thing and owl:NamedIndividual together", () => {
|
|
25
|
+
expect(pickPrimaryType([OWL_THING, NAMED_INDIVIDUAL, COMPONENT])).toBe(
|
|
26
|
+
COMPONENT,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("is deterministic among several domain types (lexicographic first)", () => {
|
|
31
|
+
expect(pickPrimaryType([COMPONENT, BLOCK])).toBe(BLOCK);
|
|
32
|
+
expect(pickPrimaryType([BLOCK, COMPONENT])).toBe(BLOCK);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("falls back to the full set when only meta-types are present", () => {
|
|
36
|
+
// Both share the owl# namespace, so "NamedIndividual" < "Thing".
|
|
37
|
+
expect(pickPrimaryType([NAMED_INDIVIDUAL, OWL_THING])).toBe(
|
|
38
|
+
NAMED_INDIVIDUAL,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NON_GROUPING_TYPE_URIS } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Choose an individual's primary type — its grouping key and `instanceOf`
|
|
5
|
+
* pointer — from its full `rdf:type` object URIs.
|
|
6
|
+
*
|
|
7
|
+
* Schema and top/individual meta-types (`owl:Class`, `owl:NamedIndividual`,
|
|
8
|
+
* `owl:Thing`, `rdf:Property`, …) are filtered out first, so a subject typed
|
|
9
|
+
* `[ds:Component, owl:NamedIndividual]` resolves to `ds:Component` rather than
|
|
10
|
+
* the meta-type that happens to sort first. Among the remaining meaningful
|
|
11
|
+
* types the lexicographically-first is chosen for determinism. When an entity
|
|
12
|
+
* has nothing but meta-types, the full set is the fallback pool so a type is
|
|
13
|
+
* still returned.
|
|
14
|
+
*
|
|
15
|
+
* @param fullTypes - The subject's full `rdf:type` object URIs.
|
|
16
|
+
* @returns The chosen full type URI, or `null` when there are no types.
|
|
17
|
+
*/
|
|
18
|
+
export default function pickPrimaryType(
|
|
19
|
+
fullTypes: readonly string[],
|
|
20
|
+
): string | null {
|
|
21
|
+
const meaningful = fullTypes.filter(
|
|
22
|
+
(type) => !NON_GROUPING_TYPE_URIS.includes(type),
|
|
23
|
+
);
|
|
24
|
+
const pool = meaningful.length > 0 ? meaningful : fullTypes;
|
|
25
|
+
return [...pool].sort((a, b) => a.localeCompare(b)).at(0) ?? null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import rankUriCompletions from "./rankUriCompletions.js";
|
|
3
|
+
import type { UriCompletionCandidate } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const candidates: UriCompletionCandidate[] = [
|
|
6
|
+
{ prefixed: "ds:Component", label: "Component" },
|
|
7
|
+
{ prefixed: "ds:global.component.button", label: "Button" },
|
|
8
|
+
{ prefixed: "ds:global.component.card", label: "Card" },
|
|
9
|
+
{ prefixed: "cs:code_purity", label: "code/function/purity" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe("rankUriCompletions", () => {
|
|
13
|
+
it("matches a URI substring case-insensitively", () => {
|
|
14
|
+
expect(rankUriCompletions(candidates, "BUTTON", 10)).toContain(
|
|
15
|
+
"ds:global.component.button",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("matches a human label fragment", () => {
|
|
20
|
+
expect(rankUriCompletions(candidates, "card", 10)).toContain(
|
|
21
|
+
"ds:global.component.card",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("ranks an exact match above a substring match", () => {
|
|
26
|
+
const ranked = rankUriCompletions(candidates, "component", 10);
|
|
27
|
+
expect(ranked.indexOf("ds:Component")).toBeLessThan(
|
|
28
|
+
ranked.indexOf("ds:global.component.button"),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("ranks a prefix match above a substring match", () => {
|
|
33
|
+
const ranked = rankUriCompletions(candidates, "comp", 10);
|
|
34
|
+
expect(ranked.indexOf("ds:Component")).toBeLessThan(
|
|
35
|
+
ranked.indexOf("ds:global.component.button"),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns the compacted URI even when matched by label", () => {
|
|
40
|
+
expect(rankUriCompletions(candidates, "purity", 10)).toEqual([
|
|
41
|
+
"cs:code_purity",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("caps the result count", () => {
|
|
46
|
+
expect(rankUriCompletions(candidates, "", 2)).toHaveLength(2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("excludes non-matches", () => {
|
|
50
|
+
expect(rankUriCompletions(candidates, "zzz", 10)).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { UriCompletionCandidate } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Match strength of a query against one field, higher is better. */
|
|
4
|
+
const NO_MATCH = 0;
|
|
5
|
+
const SUBSTRING_MATCH = 1;
|
|
6
|
+
const PREFIX_MATCH = 2;
|
|
7
|
+
const EXACT_MATCH = 3;
|
|
8
|
+
|
|
9
|
+
/** Score a single lowercased field against a lowercased query. */
|
|
10
|
+
function scoreField(field: string, query: string): number {
|
|
11
|
+
if (field === query) return EXACT_MATCH;
|
|
12
|
+
if (field.startsWith(query)) return PREFIX_MATCH;
|
|
13
|
+
if (field.includes(query)) return SUBSTRING_MATCH;
|
|
14
|
+
return NO_MATCH;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rank URI autocomplete candidates for a partial query.
|
|
19
|
+
*
|
|
20
|
+
* Matches case-insensitively against BOTH the compacted URI and the human
|
|
21
|
+
* label, so `button` completes `ds:global.component.button` and a label
|
|
22
|
+
* fragment works too. Ranks exact > prefix > substring (taking the stronger
|
|
23
|
+
* of the two fields), breaks ties by compacted URI for determinism, and caps
|
|
24
|
+
* the result at `limit`. Returns compacted URIs — the values a client reads.
|
|
25
|
+
*
|
|
26
|
+
* @param candidates - Listable subjects with their compacted URI and label.
|
|
27
|
+
* @param query - The partial text typed by the client.
|
|
28
|
+
* @param limit - Maximum suggestions to return.
|
|
29
|
+
* @returns Ranked, deduped, capped compacted URIs.
|
|
30
|
+
*/
|
|
31
|
+
export default function rankUriCompletions(
|
|
32
|
+
candidates: readonly UriCompletionCandidate[],
|
|
33
|
+
query: string,
|
|
34
|
+
limit: number,
|
|
35
|
+
): string[] {
|
|
36
|
+
const needle = query.toLowerCase();
|
|
37
|
+
|
|
38
|
+
const scored: { prefixed: string; score: number }[] = [];
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
const uriScore = scoreField(candidate.prefixed.toLowerCase(), needle);
|
|
41
|
+
const labelScore =
|
|
42
|
+
candidate.label === null
|
|
43
|
+
? NO_MATCH
|
|
44
|
+
: scoreField(candidate.label.toLowerCase(), needle);
|
|
45
|
+
const score = Math.max(uriScore, labelScore);
|
|
46
|
+
if (score > NO_MATCH) scored.push({ prefixed: candidate.prefixed, score });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
scored.sort((a, b) =>
|
|
50
|
+
b.score !== a.score
|
|
51
|
+
? b.score - a.score
|
|
52
|
+
: a.prefixed.localeCompare(b.prefixed),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const seen = new Set<string>();
|
|
56
|
+
const ranked: string[] = [];
|
|
57
|
+
for (const { prefixed } of scored) {
|
|
58
|
+
if (seen.has(prefixed)) continue;
|
|
59
|
+
seen.add(prefixed);
|
|
60
|
+
ranked.push(prefixed);
|
|
61
|
+
if (ranked.length >= limit) break;
|
|
62
|
+
}
|
|
63
|
+
return ranked;
|
|
64
|
+
}
|