@canonical/pragma-cli 0.29.0 → 0.31.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 +128 -47
- package/dist/pragma +0 -0
- package/package.json +15 -13
- package/src/completions/constants.ts +7 -0
- package/src/completions/handleQuery.test.ts +32 -0
- package/src/completions/handleQuery.ts +20 -1
- package/src/completions/queryCompletions.test.ts +5 -0
- package/src/completions/queryCompletions.ts +17 -3
- package/src/completions/querySocket.test.ts +31 -0
- package/src/completions/querySocket.ts +36 -9
- 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 +217 -0
- package/src/config/readConfig.test.ts +104 -13
- package/src/config/readConfig.ts +12 -153
- package/src/config/readConfigLayers.test.ts +109 -0
- package/src/config/readConfigLayers.ts +118 -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 +83 -1
- package/src/config/writeConfig.ts +25 -55
- package/src/constants.ts +3 -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 +2 -1
- package/src/domains/block/formatters/list.test.ts +3 -3
- package/src/domains/block/formatters/lookup.test.ts +5 -5
- package/src/domains/block/mcp/specs.ts +16 -196
- package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
- package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
- package/src/domains/block/stories.test.ts +28 -0
- package/src/domains/block/stories.ts +260 -0
- package/src/domains/config/commands/channel.test.ts +30 -3
- package/src/domains/config/commands/channel.ts +25 -8
- package/src/domains/config/commands/configScope.ts +46 -0
- package/src/domains/config/commands/framework.test.ts +32 -1
- package/src/domains/config/commands/framework.ts +26 -8
- package/src/domains/config/commands/show.ts +5 -45
- package/src/domains/config/commands/tier.test.ts +20 -2
- package/src/domains/config/commands/tier.ts +25 -8
- package/src/domains/config/commands/trace.test.ts +19 -2
- package/src/domains/config/commands/trace.ts +6 -3
- 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 +14 -11
- 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 +7 -7
- package/src/domains/config/mcp/specs.ts +33 -53
- 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/validateTier.test.ts +9 -0
- package/src/domains/config/operations/validateTier.ts +10 -2
- package/src/domains/config/stories.ts +42 -0
- package/src/domains/config/tasks/index.ts +1 -0
- package/src/domains/config/tasks/setChannel.ts +13 -28
- package/src/domains/config/tasks/setFramework.ts +11 -30
- package/src/domains/config/tasks/setTier.ts +10 -29
- package/src/domains/config/tasks/setTrace.ts +10 -28
- package/src/domains/config/tasks/writeConfigField.ts +41 -0
- package/src/domains/create/generatorPack/bundled.ts +49 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
- package/src/domains/create/index.ts +13 -7
- package/src/domains/create/mcp/specs.ts +6 -161
- package/src/domains/create/renderGeneratorUi.test.ts +103 -0
- package/src/domains/create/renderGeneratorUi.ts +65 -0
- package/src/domains/doctor/formatters/doctor.test.ts +50 -2
- package/src/domains/doctor/formatters/doctor.ts +90 -38
- package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +133 -0
- package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +32 -26
- 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 +160 -7
- package/src/domains/doctor/operations/runChecks.test.ts +15 -6
- package/src/domains/doctor/operations/runChecks.ts +2 -0
- package/src/domains/doctor/operations/types.ts +23 -1
- package/src/domains/graph/commands/inspect.test.ts +2 -2
- package/src/domains/graph/commands/query.test.ts +5 -3
- package/src/domains/graph/formatters/inspect.ts +7 -7
- package/src/domains/graph/formatters/query.test.ts +87 -0
- package/src/domains/graph/formatters/query.ts +42 -5
- package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
- package/src/domains/graph/helpers/resolveUri.ts +28 -21
- package/src/domains/graph/operations/inspectUri.test.ts +5 -5
- package/src/domains/graphql/commands/build.test.ts +5 -3
- package/src/domains/graphql/commands/build.ts +8 -16
- package/src/domains/graphql/commands/check.test.ts +5 -3
- package/src/domains/graphql/commands/check.ts +7 -13
- package/src/domains/graphql/commands/index.ts +1 -0
- package/src/domains/graphql/commands/serve.test.ts +46 -0
- package/src/domains/graphql/commands/serve.ts +141 -0
- package/src/domains/graphql/constants.ts +10 -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 +2 -0
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
- package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
- 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 +9 -6
- package/src/domains/graphql/operations/compileSchema.test.ts +23 -47
- package/src/domains/graphql/operations/compileSchema.ts +23 -25
- package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
- package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
- package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
- package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
- package/src/domains/graphql/operations/index.ts +10 -0
- package/src/domains/info/formatters/upgrade.test.ts +7 -4
- package/src/domains/info/formatters/upgrade.ts +3 -1
- package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
- package/src/domains/info/operations/collectEntityCounts.ts +123 -0
- package/src/domains/info/operations/collectInfo.test.ts +14 -3
- package/src/domains/info/operations/collectInfo.ts +12 -15
- package/src/domains/info/operations/index.ts +1 -0
- package/src/domains/info/types.ts +13 -0
- package/src/domains/llm/capabilities.test.ts +24 -4
- package/src/domains/llm/commands/capabilities.ts +12 -3
- package/src/domains/llm/commands/llm.ts +5 -1
- package/src/domains/llm/data/commandReference.ts +20 -11
- package/src/domains/llm/data/decisionTrees.ts +10 -5
- package/src/domains/llm/data/toolCatalog.ts +55 -18
- package/src/domains/llm/formatters/orientation.ts +5 -1
- package/src/domains/llm/llmCommand.test.ts +11 -3
- package/src/domains/llm/mcp/specs.ts +3 -4
- package/src/domains/llm/operations/collectContext.test.ts +64 -0
- package/src/domains/llm/operations/collectContext.ts +7 -17
- 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 +2 -1
- package/src/domains/modifier/mcp/specs.ts +15 -94
- package/src/domains/modifier/operations/lookup.ts +5 -3
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
- package/src/domains/modifier/stories.test.ts +20 -0
- package/src/domains/modifier/stories.ts +86 -0
- package/src/domains/ontology/commands/list.test.ts +5 -3
- package/src/domains/ontology/commands/list.ts +5 -58
- package/src/domains/ontology/commands/show.test.ts +85 -9
- package/src/domains/ontology/commands/show.ts +6 -58
- package/src/domains/ontology/formatters/index.ts +1 -0
- package/src/domains/ontology/formatters/list.ts +37 -12
- package/src/domains/ontology/formatters/show.ts +361 -64
- package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
- package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
- package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
- package/src/domains/ontology/helpers/index.ts +14 -0
- package/src/domains/ontology/helpers/queryClasses.ts +32 -16
- package/src/domains/ontology/helpers/queryProperties.ts +39 -6
- package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
- package/src/domains/ontology/mcp/specs.ts +7 -84
- package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
- package/src/domains/ontology/operations/listOntologies.ts +126 -30
- package/src/domains/ontology/operations/showOntology.test.ts +131 -12
- package/src/domains/ontology/operations/showOntology.ts +289 -7
- package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
- package/src/domains/ontology/stories.test.ts +36 -0
- package/src/domains/ontology/stories.ts +153 -0
- package/src/domains/refs/operations/updateRefs.ts +2 -38
- package/src/domains/setup/commands/index.ts +1 -0
- package/src/domains/setup/commands/setup.ts +70 -0
- 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/index.ts +7 -3
- package/src/domains/setup/operations/setupLsp.test.ts +3 -2
- package/src/domains/setup/operations/setupLsp.ts +7 -2
- package/src/domains/setup/operations/setupMcp.test.ts +6 -1
- package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
- package/src/domains/shared/bootStore.test.ts +82 -0
- package/src/domains/shared/bootStore.ts +57 -18
- 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 +11 -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 +23 -26
- package/src/domains/shared/extractLocalName.test.ts +3 -3
- package/src/domains/shared/formatters.ts +23 -2
- package/src/domains/shared/listTiers.ts +40 -0
- package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
- package/src/domains/shared/loaders/bundledLoader.ts +158 -12
- package/src/domains/shared/loaders/gitLoader.ts +1 -2
- package/src/domains/shared/loaders/index.ts +4 -1
- package/src/domains/shared/loaders/localLoader.ts +2 -4
- package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
- package/src/domains/shared/loaders/readPackageDir.ts +82 -9
- 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 +135 -0
- package/src/domains/shared/lookupMany.ts +50 -2
- package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
- package/src/domains/shared/mergeAndParseRefs.ts +44 -0
- package/src/domains/shared/namespaces.ts +13 -5
- package/src/domains/shared/packages.ts +10 -17
- package/src/domains/shared/parseSampleCount.test.ts +47 -0
- package/src/domains/shared/parseSampleCount.ts +45 -0
- package/src/domains/shared/prefixes.test.ts +211 -0
- package/src/domains/shared/prefixes.ts +182 -15
- package/src/domains/shared/renderers.ts +5 -5
- package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
- package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
- package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
- package/src/domains/shared/resolveLookupExitCode.ts +42 -0
- package/src/domains/shared/runtime.ts +4 -63
- package/src/domains/shared/semanticPackage.test.ts +89 -0
- package/src/domains/shared/semanticPackage.ts +34 -3
- package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
- package/src/domains/shared/stories/compileLookupCommand.ts +102 -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 +151 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +111 -0
- package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
- package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +81 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +154 -0
- package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
- package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
- package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +406 -0
- package/src/domains/shared/stories/pack/collectPackStories.ts +158 -0
- package/src/domains/shared/stories/pack/compilePackCommands.ts +50 -0
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +751 -0
- package/src/domains/shared/stories/pack/compilePackStories.ts +724 -0
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +55 -0
- package/src/domains/shared/stories/pack/index.ts +42 -0
- package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
- package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
- package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +381 -0
- package/src/domains/shared/stories/pack/types.ts +224 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +742 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +859 -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 +179 -0
- package/src/domains/shared/suggestions/expandGlob.ts +1 -0
- package/src/domains/shared/suggestions/suggestNames.ts +43 -10
- package/src/domains/shared/types/index.ts +5 -9
- package/src/domains/shared/types/ontology.ts +119 -24
- package/src/domains/shared/types/standard.ts +9 -52
- 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 +0 -1
- 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/token/commands/lookup.test.ts +8 -0
- package/src/domains/token/commands/lookup.ts +12 -0
- package/src/domains/token/commands/sample.ts +2 -1
- package/src/domains/token/featureFlag.ts +17 -0
- package/src/domains/token/index.ts +5 -0
- package/src/domains/token/mcp/specs.ts +18 -2
- package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
- package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
- package/src/domains/trace/commands/show.ts +30 -4
- package/src/domains/trace/tracePlugin.test.ts +1 -1
- package/src/index.ts +1 -13
- package/src/mcp/registerResources.test.ts +382 -74
- package/src/mcp/registerResources.ts +5 -348
- 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 +22 -5
- package/src/mcp/tools/registerTools.test.ts +167 -34
- package/src/mcp/types.ts +4 -0
- package/src/pipeline/collectCommands.test.ts +151 -1
- package/src/pipeline/collectCommands.ts +35 -13
- package/src/pipeline/createProgram.test.ts +5 -1
- package/src/pipeline/createProgram.ts +2 -2
- package/src/pipeline/firstRun.test.ts +80 -0
- package/src/pipeline/firstRun.ts +99 -0
- package/src/pipeline/parseGlobalFlags.test.ts +102 -1
- package/src/pipeline/parseGlobalFlags.ts +98 -9
- package/src/pipeline/resolveCommandKind.test.ts +12 -0
- package/src/pipeline/resolveCommandKind.ts +4 -0
- package/src/pipeline/rootHelp.test.ts +87 -0
- package/src/pipeline/rootHelp.ts +265 -0
- package/src/pipeline/runCli.orchestration.test.ts +28 -12
- package/src/pipeline/runCli.test.ts +17 -4
- package/src/pipeline/runCli.ts +202 -25
- package/src/testing/cli.ts +30 -2
- package/src/testing/createTestMcpClient.ts +4 -1
- package/src/testing/dsFixtures.ts +21 -4
- package/src/testing/index.ts +7 -0
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
- package/src/testing/integration/cli-user-stories.test.ts +63 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
- package/src/testing/integration/edge-cases.test.ts +62 -50
- package/src/testing/integration/executor-execution.test.ts +103 -0
- package/src/testing/integration/executor-golden.test.ts +145 -0
- package/src/testing/integration/mcp-surface.test.ts +40 -15
- package/src/testing/integration/parity.test.ts +136 -82
- package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
- package/src/testing/setupXdgIsolation.ts +26 -0
- package/src/testing/standardParityFixtures.ts +35 -0
- package/src/testing/store.ts +4 -4
- package/src/testing/storyFixtures.ts +63 -0
- package/src/domains/create/commands/component.test.ts +0 -166
- package/src/domains/create/commands/component.ts +0 -123
- package/src/domains/create/commands/index.ts +0 -3
- package/src/domains/create/commands/package.test.ts +0 -35
- package/src/domains/create/commands/package.ts +0 -21
- package/src/domains/create/generators.ts +0 -8
- package/src/domains/standard/commands/categories.test.ts +0 -70
- package/src/domains/standard/commands/categories.ts +0 -48
- package/src/domains/standard/commands/index.ts +0 -6
- package/src/domains/standard/commands/list.test.ts +0 -105
- package/src/domains/standard/commands/list.ts +0 -90
- package/src/domains/standard/commands/lookup.test.ts +0 -135
- package/src/domains/standard/commands/lookup.ts +0 -99
- package/src/domains/standard/commands/sample.ts +0 -57
- package/src/domains/standard/formatters/categories.test.ts +0 -44
- package/src/domains/standard/formatters/categories.ts +0 -38
- package/src/domains/standard/formatters/index.ts +0 -10
- package/src/domains/standard/formatters/list.test.ts +0 -169
- package/src/domains/standard/formatters/list.ts +0 -184
- package/src/domains/standard/formatters/lookup.test.ts +0 -95
- package/src/domains/standard/formatters/lookup.ts +0 -101
- package/src/domains/standard/formatters/sample.ts +0 -15
- package/src/domains/standard/formatters/types.ts +0 -31
- package/src/domains/standard/index.ts +0 -40
- package/src/domains/standard/mcp/index.ts +0 -2
- package/src/domains/standard/mcp/specs.ts +0 -244
- package/src/domains/standard/operations/categories.test.ts +0 -61
- package/src/domains/standard/operations/categories.ts +0 -38
- package/src/domains/standard/operations/index.ts +0 -6
- package/src/domains/standard/operations/list.test.ts +0 -75
- package/src/domains/standard/operations/list.ts +0 -64
- package/src/domains/standard/operations/lookup.test.ts +0 -58
- package/src/domains/standard/operations/lookup.ts +0 -151
- package/src/domains/standard/operations/sample.ts +0 -35
- package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
- package/src/domains/standard/orchestration/index.ts +0 -4
- package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -32
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
- package/src/domains/standard/standardConfig.ts +0 -42
- package/src/domains/standard/types.ts +0 -15
- package/src/domains/tier/commands/index.ts +0 -3
- package/src/domains/tier/commands/list.test.ts +0 -67
- package/src/domains/tier/commands/list.ts +0 -59
- package/src/domains/tier/formatters/index.ts +0 -3
- package/src/domains/tier/formatters/list.test.ts +0 -33
- package/src/domains/tier/formatters/list.ts +0 -45
- package/src/domains/tier/index.ts +0 -17
- package/src/domains/tier/mcp/index.ts +0 -2
- package/src/domains/tier/mcp/specs.ts +0 -38
- package/src/domains/tier/operations/index.ts +0 -3
- package/src/domains/tier/operations/list.test.ts +0 -32
- package/src/domains/tier/operations/list.ts +0 -33
- package/src/pipeline/runInteractiveCommand.ts +0 -198
|
@@ -28,16 +28,14 @@ export default function createLocalLoader(): PackageLoader {
|
|
|
28
28
|
// file:// refs → direct path check first
|
|
29
29
|
if (ref.kind === "file") {
|
|
30
30
|
if (!existsSync(ref.path)) return undefined;
|
|
31
|
-
|
|
32
|
-
return { name: ref.pkg, version, source: "local", graphs, skills };
|
|
31
|
+
return { name: ref.pkg, source: "local", ...readPackageDir(ref.path) };
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
// All ref kinds → try node_modules walk
|
|
36
35
|
const dir = findPackageDir(ref.pkg);
|
|
37
36
|
if (!dir) return undefined;
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
return { name: ref.pkg, version, source: "local", graphs, skills };
|
|
38
|
+
return { name: ref.pkg, source: "local", ...readPackageDir(dir) };
|
|
41
39
|
},
|
|
42
40
|
};
|
|
43
41
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import readPackageDir from "./readPackageDir.js";
|
|
6
|
+
|
|
7
|
+
let dir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
dir = mkdtempSync(join(tmpdir(), "pragma-readpkg-"));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** Write a package.json into the temp package root. */
|
|
18
|
+
function writeManifest(manifest: unknown): void {
|
|
19
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify(manifest), "utf-8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("readPackageDir — prefixes", () => {
|
|
23
|
+
it("reads package-declared prefixes from package.json pragma.prefixes", () => {
|
|
24
|
+
writeManifest({
|
|
25
|
+
version: "1.2.3",
|
|
26
|
+
pragma: { prefixes: { ds: "https://ds.canonical.com/" } },
|
|
27
|
+
});
|
|
28
|
+
const { version, prefixes } = readPackageDir(dir);
|
|
29
|
+
expect(version).toBe("1.2.3");
|
|
30
|
+
expect(prefixes).toEqual({ ds: "https://ds.canonical.com/" });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("omits prefixes when the package declares none", () => {
|
|
34
|
+
writeManifest({ version: "1.0.0" });
|
|
35
|
+
expect(readPackageDir(dir).prefixes).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("ignores a malformed pragma.prefixes field", () => {
|
|
39
|
+
writeManifest({ version: "1.0.0", pragma: { prefixes: "not-an-object" } });
|
|
40
|
+
expect(readPackageDir(dir).prefixes).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps only string-valued prefix entries", () => {
|
|
44
|
+
writeManifest({
|
|
45
|
+
version: "1.0.0",
|
|
46
|
+
pragma: { prefixes: { ds: "https://ds.canonical.com/", bad: 42 } },
|
|
47
|
+
});
|
|
48
|
+
expect(readPackageDir(dir).prefixes).toEqual({
|
|
49
|
+
ds: "https://ds.canonical.com/",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns a default version and no prefixes when package.json is absent", () => {
|
|
54
|
+
const contents = readPackageDir(dir);
|
|
55
|
+
expect(contents.version).toBe("0.0.0");
|
|
56
|
+
expect(contents.prefixes).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -16,7 +16,11 @@ import {
|
|
|
16
16
|
statSync,
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
GraphContent,
|
|
21
|
+
SkillEntry,
|
|
22
|
+
StoryFileEntry,
|
|
23
|
+
} from "../semanticPackage.js";
|
|
20
24
|
|
|
21
25
|
// ---------------------------------------------------------------------------
|
|
22
26
|
// TTL discovery convention
|
|
@@ -34,20 +38,26 @@ const TTL_DIRS: readonly { dir: string; glob: string }[] = [
|
|
|
34
38
|
export interface PackageDirContents {
|
|
35
39
|
readonly version: string;
|
|
36
40
|
readonly graphs: GraphContent[];
|
|
41
|
+
readonly prefixes?: Readonly<Record<string, string>>;
|
|
37
42
|
readonly skills: SkillEntry[];
|
|
43
|
+
readonly stories: StoryFileEntry[];
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
47
|
* Read semantic package content from a directory.
|
|
42
48
|
*
|
|
43
49
|
* @param dir - Absolute path to the package root directory.
|
|
44
|
-
* @returns Package version, TTL graph content, and
|
|
50
|
+
* @returns Package version, TTL graph content, declared prefixes, and skills.
|
|
45
51
|
*/
|
|
46
52
|
export default function readPackageDir(dir: string): PackageDirContents {
|
|
53
|
+
const manifest = readManifest(dir);
|
|
54
|
+
const prefixes = parsePrefixesField(manifest);
|
|
47
55
|
return {
|
|
48
|
-
version:
|
|
56
|
+
version: typeof manifest.version === "string" ? manifest.version : "0.0.0",
|
|
49
57
|
graphs: readGraphs(dir),
|
|
58
|
+
...(prefixes ? { prefixes } : {}),
|
|
50
59
|
skills: readSkills(dir),
|
|
60
|
+
stories: readStories(dir),
|
|
51
61
|
};
|
|
52
62
|
}
|
|
53
63
|
|
|
@@ -55,17 +65,51 @@ export default function readPackageDir(dir: string): PackageDirContents {
|
|
|
55
65
|
// Helpers
|
|
56
66
|
// ---------------------------------------------------------------------------
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
/** A package.json shape with the fields this loader reads. */
|
|
69
|
+
interface PackageManifest {
|
|
70
|
+
readonly version?: unknown;
|
|
71
|
+
readonly pragma?: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read and parse `package.json`, returning an empty object on failure. */
|
|
75
|
+
function readManifest(dir: string): PackageManifest {
|
|
60
76
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
return JSON.parse(
|
|
78
|
+
readFileSync(join(dir, "package.json"), "utf-8"),
|
|
79
|
+
) as PackageManifest;
|
|
64
80
|
} catch {
|
|
65
|
-
return
|
|
81
|
+
return {};
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Extract package-declared prefixes from a parsed manifest.
|
|
87
|
+
*
|
|
88
|
+
* Convention: a package declares its namespaces under
|
|
89
|
+
* `pragma.prefixes` in `package.json`, e.g.
|
|
90
|
+
* `{ "pragma": { "prefixes": { "ds": "https://ds.canonical.com/" } } }`.
|
|
91
|
+
* Chosen over a separate file because the manifest is already read for the
|
|
92
|
+
* version, keeping the convention zero-extra-file and discoverable. Only
|
|
93
|
+
* string→string entries are kept; malformed shapes are ignored so one bad
|
|
94
|
+
* field cannot break boot.
|
|
95
|
+
*
|
|
96
|
+
* @returns The declared prefix map, or `undefined` when none is present.
|
|
97
|
+
*/
|
|
98
|
+
function parsePrefixesField(
|
|
99
|
+
manifest: PackageManifest,
|
|
100
|
+
): Readonly<Record<string, string>> | undefined {
|
|
101
|
+
const pragma = manifest.pragma;
|
|
102
|
+
if (typeof pragma !== "object" || pragma === null) return undefined;
|
|
103
|
+
const raw = (pragma as { prefixes?: unknown }).prefixes;
|
|
104
|
+
if (typeof raw !== "object" || raw === null) return undefined;
|
|
105
|
+
|
|
106
|
+
const prefixes: Record<string, string> = {};
|
|
107
|
+
for (const [name, namespace] of Object.entries(raw)) {
|
|
108
|
+
if (typeof namespace === "string") prefixes[name] = namespace;
|
|
109
|
+
}
|
|
110
|
+
return Object.keys(prefixes).length > 0 ? prefixes : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
function readGraphs(dir: string): GraphContent[] {
|
|
70
114
|
const graphs: GraphContent[] = [];
|
|
71
115
|
|
|
@@ -87,6 +131,35 @@ function readGraphs(dir: string): GraphContent[] {
|
|
|
87
131
|
return graphs;
|
|
88
132
|
}
|
|
89
133
|
|
|
134
|
+
function readStories(dir: string): StoryFileEntry[] {
|
|
135
|
+
const storiesDir = join(dir, "stories");
|
|
136
|
+
if (!existsSync(storiesDir)) return [];
|
|
137
|
+
|
|
138
|
+
const stories: StoryFileEntry[] = [];
|
|
139
|
+
try {
|
|
140
|
+
// Sorted: readdir order is filesystem-dependent, and story collision
|
|
141
|
+
// resolution must be deterministic across platforms.
|
|
142
|
+
for (const entry of [...readdirSync(storiesDir)].sort()) {
|
|
143
|
+
if (!entry.endsWith(".json")) continue;
|
|
144
|
+
const entryPath = join(storiesDir, entry);
|
|
145
|
+
try {
|
|
146
|
+
stories.push({
|
|
147
|
+
path: entryPath,
|
|
148
|
+
definition: JSON.parse(readFileSync(entryPath, "utf-8")),
|
|
149
|
+
});
|
|
150
|
+
} catch {
|
|
151
|
+
process.stderr.write(
|
|
152
|
+
`Warning: invalid JSON in story file ${entryPath}\n`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// readdir failed — no stories
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return stories;
|
|
161
|
+
}
|
|
162
|
+
|
|
90
163
|
function readSkills(dir: string): SkillEntry[] {
|
|
91
164
|
const skillsDir = join(dir, "skills");
|
|
92
165
|
if (!existsSync(skillsDir)) return [];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { BunPlugin } from "bun";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import createStoryAssetsPlugin, {
|
|
7
|
+
STORY_ASSET_PATH_PATTERN,
|
|
8
|
+
} from "./storyAssetsPlugin.js";
|
|
9
|
+
|
|
10
|
+
interface OnLoadRegistration {
|
|
11
|
+
readonly filter: RegExp;
|
|
12
|
+
readonly callback: (args: { path: string }) => unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Run the plugin's setup against a stub builder and capture its onLoad. */
|
|
16
|
+
function captureOnLoadRegistration(): OnLoadRegistration {
|
|
17
|
+
let captured: OnLoadRegistration | undefined;
|
|
18
|
+
const builder = {
|
|
19
|
+
onLoad(
|
|
20
|
+
constraints: { filter: RegExp },
|
|
21
|
+
callback: OnLoadRegistration["callback"],
|
|
22
|
+
): void {
|
|
23
|
+
captured = { filter: constraints.filter, callback };
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
createStoryAssetsPlugin().setup(
|
|
28
|
+
builder as unknown as Parameters<BunPlugin["setup"]>[0],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!captured) throw new Error("plugin did not register an onLoad handler");
|
|
32
|
+
return captured;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("STORY_ASSET_PATH_PATTERN", () => {
|
|
36
|
+
it("matches JSON directly inside a stories directory", () => {
|
|
37
|
+
expect(
|
|
38
|
+
STORY_ASSET_PATH_PATTERN.test(
|
|
39
|
+
"/repo/node_modules/@canonical/design-system/stories/color.json",
|
|
40
|
+
),
|
|
41
|
+
).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("rejects manifests, nested files, near-miss directories, and non-JSON", () => {
|
|
45
|
+
const nonStories = [
|
|
46
|
+
"/repo/node_modules/@canonical/design-system/package.json",
|
|
47
|
+
"/repo/node_modules/@canonical/design-system/stories/nested/color.json",
|
|
48
|
+
"/repo/node_modules/@canonical/design-system/histories/color.json",
|
|
49
|
+
"/repo/node_modules/@canonical/design-system/stories/color.jsonc",
|
|
50
|
+
"/repo/node_modules/@canonical/design-system/stories/color.ttl",
|
|
51
|
+
];
|
|
52
|
+
for (const path of nonStories) {
|
|
53
|
+
expect(STORY_ASSET_PATH_PATTERN.test(path), path).toBe(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("createStoryAssetsPlugin", () => {
|
|
59
|
+
let tmpDir: string | undefined;
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
63
|
+
tmpDir = undefined;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("registers its load hook under the story path filter", () => {
|
|
67
|
+
const { filter } = captureOnLoadRegistration();
|
|
68
|
+
expect(filter).toBe(STORY_ASSET_PATH_PATTERN);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("loads story JSON contents under the file loader", () => {
|
|
72
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pragma-story-assets-"));
|
|
73
|
+
const storyPath = join(tmpDir, "color.json");
|
|
74
|
+
const content = JSON.stringify({ noun: "color" });
|
|
75
|
+
writeFileSync(storyPath, content);
|
|
76
|
+
|
|
77
|
+
const { callback } = captureOnLoadRegistration();
|
|
78
|
+
|
|
79
|
+
expect(callback({ path: storyPath })).toEqual({
|
|
80
|
+
contents: content,
|
|
81
|
+
loader: "file",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time bundler plugin: embed story-pack JSON as file assets.
|
|
3
|
+
*
|
|
4
|
+
* Bun compiles `.json` build inputs into JSON modules, which never
|
|
5
|
+
* surface in `Bun.embeddedFiles`. Story-pack files must instead be
|
|
6
|
+
* embedded as file assets — like TTL graphs — so the bundled loader can
|
|
7
|
+
* serve them inside the compiled binary. This plugin scopes the `file`
|
|
8
|
+
* loader to story paths only, leaving every other JSON input (e.g.
|
|
9
|
+
* `package.json` imported for the CLI version) compiled as a module.
|
|
10
|
+
*
|
|
11
|
+
* Contract with the bundled loader: story files are the only `.json`
|
|
12
|
+
* content embedded as file assets, so every `.json` blob in
|
|
13
|
+
* `Bun.embeddedFiles` that is not named like a package manifest is a
|
|
14
|
+
* story definition. Consequence: no module in the bundle may import a
|
|
15
|
+
* `stories/*.json` file as a JSON module — the plugin would rewrite
|
|
16
|
+
* such an import into a file-path string.
|
|
17
|
+
*
|
|
18
|
+
* Consumed by `scripts/build.ts` only; never bundled into the binary.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import type { BunPlugin } from "bun";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Matches story-pack files: a `.json` directly inside a `stories/`
|
|
26
|
+
* directory — the same non-recursive scope the local loader scans.
|
|
27
|
+
*/
|
|
28
|
+
export const STORY_ASSET_PATH_PATTERN = /[\\/]stories[\\/][^\\/]+\.json$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create the bundler plugin that embeds `stories/*.json` as file assets.
|
|
32
|
+
*
|
|
33
|
+
* @returns A `Bun.build` plugin for the compiled-binary build.
|
|
34
|
+
* @note Impure — the registered load callback reads the filesystem.
|
|
35
|
+
*/
|
|
36
|
+
export default function createStoryAssetsPlugin(): BunPlugin {
|
|
37
|
+
return {
|
|
38
|
+
name: "story-assets",
|
|
39
|
+
setup(build) {
|
|
40
|
+
build.onLoad({ filter: STORY_ASSET_PATH_PATTERN }, (args) => ({
|
|
41
|
+
contents: readFileSync(args.path, "utf-8"),
|
|
42
|
+
loader: "file",
|
|
43
|
+
}));
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PragmaError } from "#error";
|
|
3
|
+
import lookupMany, { lookupToolMeta } from "./lookupMany.js";
|
|
4
|
+
|
|
5
|
+
describe("lookupMany", () => {
|
|
6
|
+
it("returns all results when every query resolves", async () => {
|
|
7
|
+
const result = await lookupMany(["a", "b"], async (q) => q.toUpperCase());
|
|
8
|
+
|
|
9
|
+
expect(result.results).toEqual(["A", "B"]);
|
|
10
|
+
expect(result.errors).toEqual([]);
|
|
11
|
+
expect(result.meta).toEqual({ internalErrorCount: 0 });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("collects PragmaError failures as structured error entries", async () => {
|
|
15
|
+
const result = await lookupMany(["Button", "Buton"], async (q) => {
|
|
16
|
+
if (q === "Buton") {
|
|
17
|
+
throw PragmaError.notFound("block", q, { suggestions: ["Button"] });
|
|
18
|
+
}
|
|
19
|
+
return q;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(result.results).toEqual(["Button"]);
|
|
23
|
+
expect(result.errors).toEqual([
|
|
24
|
+
{
|
|
25
|
+
query: "Buton",
|
|
26
|
+
code: "ENTITY_NOT_FOUND",
|
|
27
|
+
message: 'block "Buton" not found.',
|
|
28
|
+
suggestions: ["Button"],
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
expect(result.meta).toEqual({ internalErrorCount: 0 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps N-1 results when one query throws a non-PragmaError", async () => {
|
|
35
|
+
const result = await lookupMany(
|
|
36
|
+
["a", "poisoned", "c"],
|
|
37
|
+
async (q): Promise<string> => {
|
|
38
|
+
if (q === "poisoned") {
|
|
39
|
+
throw new TypeError("cannot read properties of undefined");
|
|
40
|
+
}
|
|
41
|
+
return q.toUpperCase();
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(result.results).toEqual(["A", "C"]);
|
|
46
|
+
expect(result.errors).toEqual([
|
|
47
|
+
{
|
|
48
|
+
query: "poisoned",
|
|
49
|
+
code: "INTERNAL_ERROR",
|
|
50
|
+
message: "Internal error: cannot read properties of undefined",
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
expect(result.meta).toEqual({ internalErrorCount: 1 });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("stringifies non-Error rejection values", async () => {
|
|
57
|
+
const result = await lookupMany(["a"], async () => {
|
|
58
|
+
throw "string failure";
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.results).toEqual([]);
|
|
62
|
+
expect(result.errors).toEqual([
|
|
63
|
+
{
|
|
64
|
+
query: "a",
|
|
65
|
+
code: "INTERNAL_ERROR",
|
|
66
|
+
message: "Internal error: string failure",
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
expect(result.meta).toEqual({ internalErrorCount: 1 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("counts each internal error and mixes with PragmaError entries", async () => {
|
|
73
|
+
const result = await lookupMany(
|
|
74
|
+
["ok", "missing", "boom-1", "boom-2"],
|
|
75
|
+
async (q): Promise<string> => {
|
|
76
|
+
if (q === "missing") throw PragmaError.notFound("token", q);
|
|
77
|
+
if (q.startsWith("boom")) throw new Error(q);
|
|
78
|
+
return q;
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result.results).toEqual(["ok"]);
|
|
83
|
+
expect(result.errors).toHaveLength(3);
|
|
84
|
+
expect(result.errors.map((e) => e.code)).toEqual([
|
|
85
|
+
"ENTITY_NOT_FOUND",
|
|
86
|
+
"INTERNAL_ERROR",
|
|
87
|
+
"INTERNAL_ERROR",
|
|
88
|
+
]);
|
|
89
|
+
expect(result.meta).toEqual({ internalErrorCount: 2 });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("reclassifies a rejected SPARQL value as INVALID_INPUT, not internal", async () => {
|
|
93
|
+
const result = await lookupMany(["}; DROP"], async (q) => {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Potentially dangerous SPARQL value rejected: ${JSON.stringify(q)}`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result.results).toEqual([]);
|
|
100
|
+
expect(result.errors).toHaveLength(1);
|
|
101
|
+
expect(result.errors.at(0)?.code).toBe("INVALID_INPUT");
|
|
102
|
+
expect(result.errors.at(0)?.message).not.toContain("Internal error");
|
|
103
|
+
// A rejected value is user input, so it is not counted as an internal fault.
|
|
104
|
+
expect(result.meta).toEqual({ internalErrorCount: 0 });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("lookupToolMeta", () => {
|
|
109
|
+
it("reports count and omits internalErrorCount when zero", async () => {
|
|
110
|
+
const result = await lookupMany(["a", "b"], async (q) => q);
|
|
111
|
+
|
|
112
|
+
expect(lookupToolMeta(result)).toEqual({ count: 2 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("includes internalErrorCount when at least one query failed unexpectedly", async () => {
|
|
116
|
+
const result = await lookupMany(
|
|
117
|
+
["a", "boom"],
|
|
118
|
+
async (q): Promise<string> => {
|
|
119
|
+
if (q === "boom") throw new Error("boom");
|
|
120
|
+
return q;
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(lookupToolMeta(result)).toEqual({
|
|
125
|
+
count: 1,
|
|
126
|
+
internalErrorCount: 1,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("tolerates hand-built results without meta", () => {
|
|
131
|
+
expect(lookupToolMeta({ results: ["x"], errors: [] })).toEqual({
|
|
132
|
+
count: 1,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { PragmaError } from "#error";
|
|
2
2
|
import type { LookupResult } from "./contracts.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Marker in the ke SPARQL guard's rejection message. A rejected value is user
|
|
6
|
+
* input, not an internal fault, so it is reclassified as INVALID_INPUT rather
|
|
7
|
+
* than mislabelled as an internal error.
|
|
8
|
+
*/
|
|
9
|
+
const SPARQL_REJECTION_MARKER = "Potentially dangerous SPARQL value rejected";
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Resolve multiple lookup queries and collect structured per-query failures.
|
|
6
13
|
*
|
|
7
14
|
* Used by MCP lookup tools so a single call can return multiple results
|
|
8
15
|
* without failing the entire request when one query is missing.
|
|
16
|
+
*
|
|
17
|
+
* The collection loop never rejects: a `PragmaError` becomes an error entry
|
|
18
|
+
* with its own code, and any other thrown value becomes an `INTERNAL_ERROR`
|
|
19
|
+
* entry — so one poisoned query cannot discard the other queries' results.
|
|
20
|
+
* `meta.internalErrorCount` reports how many entries are internal errors.
|
|
9
21
|
*/
|
|
10
22
|
export default async function lookupMany<TResult>(
|
|
11
23
|
queries: readonly string[],
|
|
@@ -21,6 +33,7 @@ export default async function lookupMany<TResult>(
|
|
|
21
33
|
queries.map((query) => lookup(query)),
|
|
22
34
|
);
|
|
23
35
|
const results: TResult[] = [];
|
|
36
|
+
let internalErrorCount = 0;
|
|
24
37
|
|
|
25
38
|
for (const [index, outcome] of settled.entries()) {
|
|
26
39
|
const query = queries[index];
|
|
@@ -45,9 +58,44 @@ export default async function lookupMany<TResult>(
|
|
|
45
58
|
}),
|
|
46
59
|
});
|
|
47
60
|
} else {
|
|
48
|
-
|
|
61
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
62
|
+
if (reason.includes(SPARQL_REJECTION_MARKER)) {
|
|
63
|
+
// A rejected value is invalid user input, not an internal fault.
|
|
64
|
+
errors.push({
|
|
65
|
+
query,
|
|
66
|
+
code: "INVALID_INPUT",
|
|
67
|
+
message: `Invalid lookup value "${query}": rejected as unsafe.`,
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
internalErrorCount += 1;
|
|
71
|
+
errors.push({
|
|
72
|
+
query,
|
|
73
|
+
code: "INTERNAL_ERROR",
|
|
74
|
+
message: `Internal error: ${reason}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
return { results, errors };
|
|
80
|
+
return { results, errors, meta: { internalErrorCount } };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build MCP envelope metadata for a lookup result.
|
|
85
|
+
*
|
|
86
|
+
* Always reports `count`; includes `internalErrorCount` only when at least
|
|
87
|
+
* one query failed unexpectedly, so healthy responses stay unchanged.
|
|
88
|
+
*
|
|
89
|
+
* @param result - A lookup result, typically produced via {@link lookupMany}.
|
|
90
|
+
* @returns Meta object for the `{ ok, data, meta }` tool envelope.
|
|
91
|
+
*/
|
|
92
|
+
export function lookupToolMeta<TResult>(result: LookupResult<TResult>): {
|
|
93
|
+
count: number;
|
|
94
|
+
internalErrorCount?: number;
|
|
95
|
+
} {
|
|
96
|
+
const internalErrorCount = result.meta?.internalErrorCount ?? 0;
|
|
97
|
+
return {
|
|
98
|
+
count: result.results.length,
|
|
99
|
+
...(internalErrorCount > 0 && { internalErrorCount }),
|
|
100
|
+
};
|
|
53
101
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import type { RawPackageEntry } from "../refs/operations/parseRef.js";
|
|
6
|
+
import { mergeAndParseRefs } from "./mergeAndParseRefs.js";
|
|
7
|
+
import { DEFAULT_PACKAGES } from "./packages.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_NAMES = DEFAULT_PACKAGES.map((entry) =>
|
|
10
|
+
typeof entry === "string" ? entry : entry.name,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
function writeGlobalRefs(
|
|
14
|
+
xdgDir: string,
|
|
15
|
+
packages: ReadonlyArray<RawPackageEntry>,
|
|
16
|
+
): void {
|
|
17
|
+
const dir = join(xdgDir, "pragma");
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
writeFileSync(join(dir, "refs.json"), JSON.stringify({ packages }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("mergeAndParseRefs", () => {
|
|
23
|
+
let tmpDir: string;
|
|
24
|
+
const origEnv = { ...process.env };
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pragma-merge-refs-"));
|
|
28
|
+
// Point XDG_CONFIG_HOME to our temp dir so readGlobalRefs() resolves there
|
|
29
|
+
process.env.XDG_CONFIG_HOME = tmpDir;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
process.env = { ...origEnv };
|
|
34
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns parsed defaults when config and global refs are absent", () => {
|
|
38
|
+
const refs = mergeAndParseRefs(undefined);
|
|
39
|
+
expect(refs.map((ref) => ref.pkg)).toEqual(DEFAULT_NAMES);
|
|
40
|
+
expect(refs.every((ref) => ref.kind === "git")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("treats an explicitly empty config list as not configured", () => {
|
|
44
|
+
expect(mergeAndParseRefs([])).toEqual(mergeAndParseRefs(undefined));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("falls back to global refs when the config list is empty", () => {
|
|
48
|
+
writeGlobalRefs(tmpDir, [
|
|
49
|
+
{ name: "@canonical/design-system", source: "file:///tmp/ds" },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const refs = mergeAndParseRefs([]);
|
|
53
|
+
|
|
54
|
+
expect(refs.map((ref) => ref.pkg)).toEqual(DEFAULT_NAMES);
|
|
55
|
+
expect(refs.at(0)).toEqual({
|
|
56
|
+
kind: "file",
|
|
57
|
+
pkg: "@canonical/design-system",
|
|
58
|
+
path: "/tmp/ds",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("merges global refs over defaults by package name", () => {
|
|
63
|
+
writeGlobalRefs(tmpDir, [
|
|
64
|
+
{ name: "@canonical/design-system", source: "file:///tmp/ds" },
|
|
65
|
+
"@acme/extra",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const refs = mergeAndParseRefs(undefined);
|
|
69
|
+
|
|
70
|
+
expect(refs.map((ref) => ref.pkg)).toEqual([
|
|
71
|
+
...DEFAULT_NAMES,
|
|
72
|
+
"@acme/extra",
|
|
73
|
+
]);
|
|
74
|
+
expect(refs.at(0)?.kind).toBe("file");
|
|
75
|
+
expect(refs.at(-1)).toEqual({ kind: "npm", pkg: "@acme/extra" });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("replaces defaults and global refs with a non-empty config list", () => {
|
|
79
|
+
writeGlobalRefs(tmpDir, ["@acme/global-only"]);
|
|
80
|
+
|
|
81
|
+
const refs = mergeAndParseRefs([
|
|
82
|
+
{ name: "@acme/only", source: "file:///tmp/only" },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
expect(refs).toEqual([
|
|
86
|
+
{ kind: "file", pkg: "@acme/only", path: "/tmp/only" },
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
});
|