@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
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import { PragmaError } from "#error";
|
|
2
|
+
import { MAX_SAMPLE_COUNT, MIN_SAMPLE_COUNT } from "../../parseSampleCount.js";
|
|
3
|
+
import type {
|
|
4
|
+
StoryPackColumn,
|
|
5
|
+
StoryPackDefinition,
|
|
6
|
+
StoryPackDisclosure,
|
|
7
|
+
StoryPackExpand,
|
|
8
|
+
StoryPackExpandField,
|
|
9
|
+
StoryPackField,
|
|
10
|
+
StoryPackFilter,
|
|
11
|
+
StoryPackList,
|
|
12
|
+
StoryPackLookup,
|
|
13
|
+
StoryPackSample,
|
|
14
|
+
StoryPackSearch,
|
|
15
|
+
StoryPackSection,
|
|
16
|
+
StoryPackVerb,
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
|
|
19
|
+
const NOUN_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
20
|
+
const FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Object-prototype key names no pack identifier may take.
|
|
24
|
+
*
|
|
25
|
+
* Pack identifiers become plain-object keys at run time — SELECT
|
|
26
|
+
* variables become row keys, expand names become entity keys, and filter
|
|
27
|
+
* params become parameter keys — so `__proto__`/`constructor`/`prototype`
|
|
28
|
+
* are rejected at boot, before any row or params object could be built
|
|
29
|
+
* with a prototype-polluting key.
|
|
30
|
+
*/
|
|
31
|
+
const RESERVED_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reject an identifier that names a reserved object-prototype key.
|
|
35
|
+
*
|
|
36
|
+
* Shared guard for every FIELD_PATTERN/FILTER_PARAM_PATTERN-shaped
|
|
37
|
+
* identifier (column fields, field/section/expand/select names, filter
|
|
38
|
+
* params and variables, search variables, disclosure levels).
|
|
39
|
+
*
|
|
40
|
+
* @throws PragmaError with code `CONFIG_ERROR` when the value is reserved.
|
|
41
|
+
*/
|
|
42
|
+
function rejectReservedObjectKey(
|
|
43
|
+
value: string,
|
|
44
|
+
where: string,
|
|
45
|
+
source: string,
|
|
46
|
+
): void {
|
|
47
|
+
if (RESERVED_OBJECT_KEYS.has(value)) {
|
|
48
|
+
throw buildStoryConfigError(
|
|
49
|
+
source,
|
|
50
|
+
`"${where}" must not be the reserved object key "${value}".`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** A prefixed name (`ex:Recipe`) — prefix + local part, no path slashes. */
|
|
55
|
+
const PREFIXED_NAME_PATTERN = /^[A-Za-z][\w-]*:[^/<>"\s]+$/;
|
|
56
|
+
|
|
57
|
+
/** An absolute IRI that can be safely embedded as `<iri>` in SPARQL. */
|
|
58
|
+
const EMBEDDABLE_IRI_PATTERN = /^[A-Za-z][\w+.-]*:\/\/[^<>"\s]+$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a resolved IRI matches the embeddable shape pack terms are held
|
|
62
|
+
* to (no whitespace or `<>"` characters, `scheme://` form).
|
|
63
|
+
*
|
|
64
|
+
* Shared with the lookup path so an IRI-addressed lookup applies the same
|
|
65
|
+
* strictness to resolved user queries as this validator applies to
|
|
66
|
+
* authored pack terms before either is embedded as `<iri>` in SPARQL.
|
|
67
|
+
*/
|
|
68
|
+
export function isEmbeddableIri(value: string): boolean {
|
|
69
|
+
return EMBEDDABLE_IRI_PATTERN.test(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A filter parameter name: a single lowercase word.
|
|
74
|
+
*
|
|
75
|
+
* Deliberately stricter than a noun (no hyphens): the name is used
|
|
76
|
+
* verbatim as the CLI flag, the Commander result key, and the MCP
|
|
77
|
+
* parameter key. A hyphenated name would register as `--meal-type` but
|
|
78
|
+
* be read back by Commander under the camelCased `mealType`, so the
|
|
79
|
+
* filter would silently never apply on the CLI while working on MCP.
|
|
80
|
+
* Restricting to one lowercase word keeps all three identical.
|
|
81
|
+
*/
|
|
82
|
+
const FILTER_PARAM_PATTERN = /^[a-z][a-z0-9]*$/;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parameter names taken by global flags or parameters the kernel appends
|
|
86
|
+
* to every read story — `condensed` is added to every MCP list tool,
|
|
87
|
+
* `detailed` to every lookup, `search` is the compiled free-text search
|
|
88
|
+
* parameter, and `detail` the disclosure level selector — so a filter may
|
|
89
|
+
* not reuse them.
|
|
90
|
+
*/
|
|
91
|
+
const RESERVED_FILTER_PARAMS = new Set([
|
|
92
|
+
"llm",
|
|
93
|
+
"format",
|
|
94
|
+
"verbose",
|
|
95
|
+
"detail",
|
|
96
|
+
"detailed",
|
|
97
|
+
"condensed",
|
|
98
|
+
"names",
|
|
99
|
+
"search",
|
|
100
|
+
"help",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Names a disclosure level may not take. Levels derive parameters — the
|
|
105
|
+
* `--detail <level>` selector plus one boolean alias flag per non-base
|
|
106
|
+
* level — so a level must not collide with `detail` itself, the lookup's
|
|
107
|
+
* `names` positional, or kernel/global parameter names. `detailed` is
|
|
108
|
+
* deliberately absent: it is the expected name for a pack's legacy
|
|
109
|
+
* detailed level and derives the compatible `--detailed`/`detailed`
|
|
110
|
+
* alias.
|
|
111
|
+
*/
|
|
112
|
+
const RESERVED_LEVEL_NAMES = new Set([
|
|
113
|
+
"detail",
|
|
114
|
+
"names",
|
|
115
|
+
"condensed",
|
|
116
|
+
"search",
|
|
117
|
+
"llm",
|
|
118
|
+
"format",
|
|
119
|
+
"verbose",
|
|
120
|
+
"help",
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validate one raw story-pack definition.
|
|
125
|
+
*
|
|
126
|
+
* Fails fast with `CONFIG_ERROR` naming the source and the offending
|
|
127
|
+
* field, mirroring how the `packages` config field is validated.
|
|
128
|
+
*
|
|
129
|
+
* @param raw - Parsed JSON value to validate.
|
|
130
|
+
* @param source - Where the definition came from, for error messages.
|
|
131
|
+
* @returns The validated definition.
|
|
132
|
+
* @throws PragmaError with code `CONFIG_ERROR` when the shape is invalid.
|
|
133
|
+
*/
|
|
134
|
+
export default function validateStoryPackDefinition(
|
|
135
|
+
raw: unknown,
|
|
136
|
+
source: string,
|
|
137
|
+
): StoryPackDefinition {
|
|
138
|
+
const obj = requireObject(raw, "story", source);
|
|
139
|
+
|
|
140
|
+
const noun = requireString(obj.noun, "noun", source);
|
|
141
|
+
if (!NOUN_PATTERN.test(noun)) {
|
|
142
|
+
throw buildStoryConfigError(
|
|
143
|
+
source,
|
|
144
|
+
`"noun" must be kebab-case, got "${noun}".`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const description =
|
|
149
|
+
obj.description === undefined
|
|
150
|
+
? undefined
|
|
151
|
+
: requireString(obj.description, "description", source);
|
|
152
|
+
const toolDescription =
|
|
153
|
+
obj.toolDescription === undefined
|
|
154
|
+
? undefined
|
|
155
|
+
: requireString(obj.toolDescription, "toolDescription", source);
|
|
156
|
+
|
|
157
|
+
const list = validateList(obj.list, source);
|
|
158
|
+
const verbs = validateVerbs(obj.verbs, source);
|
|
159
|
+
const lookup =
|
|
160
|
+
obj.lookup === undefined ? undefined : validateLookup(obj.lookup, source);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
noun,
|
|
164
|
+
...(description !== undefined ? { description } : {}),
|
|
165
|
+
...(toolDescription !== undefined ? { toolDescription } : {}),
|
|
166
|
+
list,
|
|
167
|
+
...(verbs ? { verbs } : {}),
|
|
168
|
+
...(lookup ? { lookup } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Verbs the compiler itself emits for a pack noun — an extra verb may not
|
|
174
|
+
* collide with them.
|
|
175
|
+
*/
|
|
176
|
+
const COMPILED_VERBS = new Set(["list", "lookup", "sample"]);
|
|
177
|
+
|
|
178
|
+
function validateVerbs(
|
|
179
|
+
raw: unknown,
|
|
180
|
+
source: string,
|
|
181
|
+
): readonly StoryPackVerb[] | undefined {
|
|
182
|
+
if (raw === undefined) return undefined;
|
|
183
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
184
|
+
throw buildStoryConfigError(source, '"verbs" must be a non-empty array.');
|
|
185
|
+
}
|
|
186
|
+
const seen = new Set<string>();
|
|
187
|
+
return raw.map((entry, index) => {
|
|
188
|
+
const where = `verbs[${index}]`;
|
|
189
|
+
const obj = requireObject(entry, where, source);
|
|
190
|
+
const verb = requireString(obj.verb, `${where}.verb`, source);
|
|
191
|
+
if (!NOUN_PATTERN.test(verb)) {
|
|
192
|
+
throw buildStoryConfigError(
|
|
193
|
+
source,
|
|
194
|
+
`"${where}.verb" must be kebab-case, got "${verb}".`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (COMPILED_VERBS.has(verb)) {
|
|
198
|
+
throw buildStoryConfigError(
|
|
199
|
+
source,
|
|
200
|
+
`"${where}.verb" "${verb}" collides with a compiled verb.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (seen.has(verb)) {
|
|
204
|
+
throw buildStoryConfigError(source, `duplicate verb "${verb}".`);
|
|
205
|
+
}
|
|
206
|
+
seen.add(verb);
|
|
207
|
+
const description =
|
|
208
|
+
obj.description === undefined
|
|
209
|
+
? undefined
|
|
210
|
+
: requireString(obj.description, `${where}.description`, source);
|
|
211
|
+
const toolDescription =
|
|
212
|
+
obj.toolDescription === undefined
|
|
213
|
+
? undefined
|
|
214
|
+
: requireString(
|
|
215
|
+
obj.toolDescription,
|
|
216
|
+
`${where}.toolDescription`,
|
|
217
|
+
source,
|
|
218
|
+
);
|
|
219
|
+
return {
|
|
220
|
+
verb,
|
|
221
|
+
...(description !== undefined ? { description } : {}),
|
|
222
|
+
...(toolDescription !== undefined ? { toolDescription } : {}),
|
|
223
|
+
...validateListShape(entry, where, source),
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function validateList(
|
|
229
|
+
raw: unknown,
|
|
230
|
+
source: string,
|
|
231
|
+
): StoryPackDefinition["list"] {
|
|
232
|
+
return validateListShape(raw, "list", source);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Validate a list-shaped story half: query, columns, filters, and search.
|
|
237
|
+
*
|
|
238
|
+
* Shared by `list` and (pack v1) extra list verbs, which compile through
|
|
239
|
+
* the same machinery. `where` names the field path for error messages.
|
|
240
|
+
*/
|
|
241
|
+
function validateListShape(
|
|
242
|
+
raw: unknown,
|
|
243
|
+
where: string,
|
|
244
|
+
source: string,
|
|
245
|
+
): StoryPackList {
|
|
246
|
+
const obj = requireObject(raw, where, source);
|
|
247
|
+
const query = requireString(obj.query, `${where}.query`, source);
|
|
248
|
+
// Strip leading PREFIX declarations so the real query verb is checked —
|
|
249
|
+
// "PREFIX ex: <…> CONSTRUCT {…}" must fail here, not at first use.
|
|
250
|
+
const afterPrefixes = query.replace(
|
|
251
|
+
/^(\s*PREFIX\s+[A-Za-z][\w-]*:\s*<[^>]*>)+/i,
|
|
252
|
+
"",
|
|
253
|
+
);
|
|
254
|
+
if (!/^\s*SELECT\s/i.test(afterPrefixes)) {
|
|
255
|
+
throw buildStoryConfigError(
|
|
256
|
+
source,
|
|
257
|
+
`"${where}.query" must be a SPARQL SELECT query.`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!Array.isArray(obj.columns) || obj.columns.length === 0) {
|
|
262
|
+
throw buildStoryConfigError(
|
|
263
|
+
source,
|
|
264
|
+
`"${where}.columns" must be a non-empty array.`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const columns = obj.columns.map((column, index) =>
|
|
268
|
+
validateColumn(column, `${where}.columns[${index}]`, source),
|
|
269
|
+
);
|
|
270
|
+
// Like filters and search below: a column naming a variable the query
|
|
271
|
+
// never binds is a typo that should fail at boot, not render an
|
|
272
|
+
// always-empty column at first use.
|
|
273
|
+
for (const column of columns) {
|
|
274
|
+
if (!referencesVariable(query, column.field)) {
|
|
275
|
+
throw buildStoryConfigError(
|
|
276
|
+
source,
|
|
277
|
+
`column field "?${column.field}" does not appear in "${where}.query".`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const filters = validateFilterArray(obj.filters, where, source);
|
|
283
|
+
for (const filter of filters ?? []) {
|
|
284
|
+
if (!referencesVariable(query, filter.variable)) {
|
|
285
|
+
throw buildStoryConfigError(
|
|
286
|
+
source,
|
|
287
|
+
`filter variable "?${filter.variable}" does not appear in "${where}.query".`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const search = validateSearch(obj.search, query, where, source);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
query,
|
|
296
|
+
columns,
|
|
297
|
+
...(filters ? { filters } : {}),
|
|
298
|
+
...(search ? { search } : {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Validate a list's free-text `search` declaration.
|
|
304
|
+
*
|
|
305
|
+
* Every searched variable must appear in the query — like filters, this
|
|
306
|
+
* fails a typo at boot instead of a search that silently never matches.
|
|
307
|
+
*/
|
|
308
|
+
function validateSearch(
|
|
309
|
+
raw: unknown,
|
|
310
|
+
query: string,
|
|
311
|
+
where: string,
|
|
312
|
+
source: string,
|
|
313
|
+
): StoryPackSearch | undefined {
|
|
314
|
+
if (raw === undefined) return undefined;
|
|
315
|
+
const obj = requireObject(raw, `${where}.search`, source);
|
|
316
|
+
if (!Array.isArray(obj.variables) || obj.variables.length === 0) {
|
|
317
|
+
throw buildStoryConfigError(
|
|
318
|
+
source,
|
|
319
|
+
`"${where}.search.variables" must be a non-empty array.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const variables = obj.variables.map((value, index) => {
|
|
323
|
+
const variable = requireString(
|
|
324
|
+
value,
|
|
325
|
+
`${where}.search.variables[${index}]`,
|
|
326
|
+
source,
|
|
327
|
+
);
|
|
328
|
+
if (!FIELD_PATTERN.test(variable)) {
|
|
329
|
+
throw buildStoryConfigError(
|
|
330
|
+
source,
|
|
331
|
+
`"${where}.search.variables[${index}]" must name a SELECT variable.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
rejectReservedObjectKey(
|
|
335
|
+
variable,
|
|
336
|
+
`${where}.search.variables[${index}]`,
|
|
337
|
+
source,
|
|
338
|
+
);
|
|
339
|
+
if (!referencesVariable(query, variable)) {
|
|
340
|
+
throw buildStoryConfigError(
|
|
341
|
+
source,
|
|
342
|
+
`search variable "?${variable}" does not appear in "${where}.query".`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return variable;
|
|
346
|
+
});
|
|
347
|
+
const description =
|
|
348
|
+
obj.description === undefined
|
|
349
|
+
? undefined
|
|
350
|
+
: requireString(obj.description, `${where}.search.description`, source);
|
|
351
|
+
return {
|
|
352
|
+
variables,
|
|
353
|
+
...(description !== undefined ? { description } : {}),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Check that the query mentions the variable (`?name` or `$name`).
|
|
359
|
+
*
|
|
360
|
+
* A textual check, not a SPARQL parse — it exists to fail typos at boot
|
|
361
|
+
* instead of silently filtering every row out at first use.
|
|
362
|
+
*/
|
|
363
|
+
function referencesVariable(query: string, variable: string): boolean {
|
|
364
|
+
const pattern = new RegExp(`[?$]${variable}\\b`);
|
|
365
|
+
return pattern.test(query);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function validateFilterArray(
|
|
369
|
+
raw: unknown,
|
|
370
|
+
where: string,
|
|
371
|
+
source: string,
|
|
372
|
+
): readonly StoryPackFilter[] | undefined {
|
|
373
|
+
if (raw === undefined) return undefined;
|
|
374
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
375
|
+
throw buildStoryConfigError(
|
|
376
|
+
source,
|
|
377
|
+
`"${where}.filters" must be a non-empty array.`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const filters = raw.map((entry, index) =>
|
|
381
|
+
validateFilter(entry, `${where}.filters[${index}]`, source),
|
|
382
|
+
);
|
|
383
|
+
const seen = new Set<string>();
|
|
384
|
+
for (const filter of filters) {
|
|
385
|
+
if (seen.has(filter.param)) {
|
|
386
|
+
throw buildStoryConfigError(
|
|
387
|
+
source,
|
|
388
|
+
`duplicate filter param "${filter.param}".`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
seen.add(filter.param);
|
|
392
|
+
}
|
|
393
|
+
return filters;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function validateFilter(
|
|
397
|
+
raw: unknown,
|
|
398
|
+
where: string,
|
|
399
|
+
source: string,
|
|
400
|
+
): StoryPackFilter {
|
|
401
|
+
const obj = requireObject(raw, where, source);
|
|
402
|
+
const param = requireString(obj.param, `${where}.param`, source);
|
|
403
|
+
if (!FILTER_PARAM_PATTERN.test(param)) {
|
|
404
|
+
throw buildStoryConfigError(
|
|
405
|
+
source,
|
|
406
|
+
`"${where}.param" must be a single lowercase word (letters and digits, no hyphens), got "${param}".`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
if (RESERVED_FILTER_PARAMS.has(param)) {
|
|
410
|
+
throw buildStoryConfigError(
|
|
411
|
+
source,
|
|
412
|
+
`"${where}.param" "${param}" is a reserved name.`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
rejectReservedObjectKey(param, `${where}.param`, source);
|
|
416
|
+
const variable = requireString(obj.variable, `${where}.variable`, source);
|
|
417
|
+
if (!FIELD_PATTERN.test(variable)) {
|
|
418
|
+
throw buildStoryConfigError(
|
|
419
|
+
source,
|
|
420
|
+
`"${where}.variable" must name a SELECT variable.`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
rejectReservedObjectKey(variable, `${where}.variable`, source);
|
|
424
|
+
// `values` is optional (pack v1): without it the filter is a free-string
|
|
425
|
+
// parameter matched case-insensitively against the variable — used when
|
|
426
|
+
// the value set is data-driven and cannot be declared.
|
|
427
|
+
const values =
|
|
428
|
+
obj.values === undefined
|
|
429
|
+
? undefined
|
|
430
|
+
: validateFilterValues(obj.values, where, source);
|
|
431
|
+
const description =
|
|
432
|
+
obj.description === undefined
|
|
433
|
+
? undefined
|
|
434
|
+
: requireString(obj.description, `${where}.description`, source);
|
|
435
|
+
return {
|
|
436
|
+
param,
|
|
437
|
+
variable,
|
|
438
|
+
...(values ? { values } : {}),
|
|
439
|
+
...(description !== undefined ? { description } : {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function validateFilterValues(
|
|
444
|
+
raw: unknown,
|
|
445
|
+
where: string,
|
|
446
|
+
source: string,
|
|
447
|
+
): readonly string[] {
|
|
448
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
449
|
+
throw buildStoryConfigError(
|
|
450
|
+
source,
|
|
451
|
+
`"${where}.values" must be a non-empty array.`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const values = raw.map((value, index) =>
|
|
455
|
+
requireString(value, `${where}.values[${index}]`, source),
|
|
456
|
+
);
|
|
457
|
+
const lowered = new Set<string>();
|
|
458
|
+
for (const value of values) {
|
|
459
|
+
const key = value.toLowerCase();
|
|
460
|
+
if (lowered.has(key)) {
|
|
461
|
+
throw buildStoryConfigError(
|
|
462
|
+
source,
|
|
463
|
+
`"${where}.values" contains duplicate value "${value}" ` +
|
|
464
|
+
"(values must be unique, case-insensitively).",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
lowered.add(key);
|
|
468
|
+
}
|
|
469
|
+
return values;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function validateColumn(
|
|
473
|
+
raw: unknown,
|
|
474
|
+
where: string,
|
|
475
|
+
source: string,
|
|
476
|
+
): StoryPackColumn {
|
|
477
|
+
const obj = requireObject(raw, where, source);
|
|
478
|
+
const field = requireString(obj.field, `${where}.field`, source);
|
|
479
|
+
if (!FIELD_PATTERN.test(field)) {
|
|
480
|
+
throw buildStoryConfigError(
|
|
481
|
+
source,
|
|
482
|
+
`"${where}.field" must name a SELECT variable.`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
rejectReservedObjectKey(field, `${where}.field`, source);
|
|
486
|
+
const label =
|
|
487
|
+
obj.label === undefined
|
|
488
|
+
? undefined
|
|
489
|
+
: requireString(obj.label, `${where}.label`, source);
|
|
490
|
+
return { field, ...(label !== undefined ? { label } : {}) };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function validateLookup(raw: unknown, source: string): StoryPackLookup {
|
|
494
|
+
const obj = requireObject(raw, "lookup", source);
|
|
495
|
+
const by = requirePredicateTerm(obj.by, "lookup.by", source);
|
|
496
|
+
const type =
|
|
497
|
+
obj.type === undefined
|
|
498
|
+
? undefined
|
|
499
|
+
: requireTerm(obj.type, "lookup.type", source);
|
|
500
|
+
const description =
|
|
501
|
+
obj.description === undefined
|
|
502
|
+
? undefined
|
|
503
|
+
: requireString(obj.description, "lookup.description", source);
|
|
504
|
+
const toolDescription =
|
|
505
|
+
obj.toolDescription === undefined
|
|
506
|
+
? undefined
|
|
507
|
+
: requireString(obj.toolDescription, "lookup.toolDescription", source);
|
|
508
|
+
|
|
509
|
+
const fields = validateFieldArray(obj.fields, "lookup.fields", source);
|
|
510
|
+
const sections = validateSectionArray(
|
|
511
|
+
obj.sections,
|
|
512
|
+
"lookup.sections",
|
|
513
|
+
source,
|
|
514
|
+
);
|
|
515
|
+
const expand = validateExpandArray(obj.expand, "lookup.expand", source);
|
|
516
|
+
const disclosure = validateDisclosure(obj.disclosure, source);
|
|
517
|
+
const sample = validateSample(obj.sample, source);
|
|
518
|
+
|
|
519
|
+
// Cross-check: every expand `level` must name a declared disclosure level.
|
|
520
|
+
const levels = new Set(disclosure?.levels ?? []);
|
|
521
|
+
for (const entry of expand ?? []) {
|
|
522
|
+
if (entry.level !== undefined && !levels.has(entry.level)) {
|
|
523
|
+
throw buildStoryConfigError(
|
|
524
|
+
source,
|
|
525
|
+
`expand "${entry.name}" level "${entry.level}" is not a declared disclosure level.`,
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
by,
|
|
532
|
+
...(type !== undefined ? { type } : {}),
|
|
533
|
+
...(description !== undefined ? { description } : {}),
|
|
534
|
+
...(toolDescription !== undefined ? { toolDescription } : {}),
|
|
535
|
+
...(fields ? { fields } : {}),
|
|
536
|
+
...(sections ? { sections } : {}),
|
|
537
|
+
...(expand ? { expand } : {}),
|
|
538
|
+
...(disclosure ? { disclosure } : {}),
|
|
539
|
+
...(sample !== undefined ? { sample } : {}),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function validateSample(
|
|
544
|
+
raw: unknown,
|
|
545
|
+
source: string,
|
|
546
|
+
): true | StoryPackSample | undefined {
|
|
547
|
+
if (raw === undefined) return undefined;
|
|
548
|
+
if (raw === true) return true;
|
|
549
|
+
const obj = requireObject(raw, "lookup.sample", source);
|
|
550
|
+
if (obj.count !== undefined) {
|
|
551
|
+
// Authors fail fast on an out-of-range default; user-provided counts
|
|
552
|
+
// are clamped at run time by parseSampleCount instead.
|
|
553
|
+
if (
|
|
554
|
+
typeof obj.count !== "number" ||
|
|
555
|
+
!Number.isInteger(obj.count) ||
|
|
556
|
+
obj.count < MIN_SAMPLE_COUNT ||
|
|
557
|
+
obj.count > MAX_SAMPLE_COUNT
|
|
558
|
+
) {
|
|
559
|
+
throw buildStoryConfigError(
|
|
560
|
+
source,
|
|
561
|
+
`"lookup.sample.count" must be an integer between ${MIN_SAMPLE_COUNT} and ${MAX_SAMPLE_COUNT}.`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const description =
|
|
566
|
+
obj.description === undefined
|
|
567
|
+
? undefined
|
|
568
|
+
: requireString(obj.description, "lookup.sample.description", source);
|
|
569
|
+
const toolDescription =
|
|
570
|
+
obj.toolDescription === undefined
|
|
571
|
+
? undefined
|
|
572
|
+
: requireString(
|
|
573
|
+
obj.toolDescription,
|
|
574
|
+
"lookup.sample.toolDescription",
|
|
575
|
+
source,
|
|
576
|
+
);
|
|
577
|
+
return {
|
|
578
|
+
...(obj.count !== undefined ? { count: obj.count as number } : {}),
|
|
579
|
+
...(description !== undefined ? { description } : {}),
|
|
580
|
+
...(toolDescription !== undefined ? { toolDescription } : {}),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function validateDisclosure(
|
|
585
|
+
raw: unknown,
|
|
586
|
+
source: string,
|
|
587
|
+
): StoryPackDisclosure | undefined {
|
|
588
|
+
if (raw === undefined) return undefined;
|
|
589
|
+
const obj = requireObject(raw, "lookup.disclosure", source);
|
|
590
|
+
if (!Array.isArray(obj.levels) || obj.levels.length === 0) {
|
|
591
|
+
throw buildStoryConfigError(
|
|
592
|
+
source,
|
|
593
|
+
'"lookup.disclosure.levels" must be a non-empty array.',
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const seen = new Set<string>();
|
|
597
|
+
const levels = obj.levels.map((value, index) => {
|
|
598
|
+
const level = requireString(
|
|
599
|
+
value,
|
|
600
|
+
`lookup.disclosure.levels[${index}]`,
|
|
601
|
+
source,
|
|
602
|
+
);
|
|
603
|
+
// Levels derive parameter names (`--detail` enum values and the alias
|
|
604
|
+
// flags), so they are held to the same single-word rule as filter
|
|
605
|
+
// params and may not shadow reserved parameter names.
|
|
606
|
+
if (!FILTER_PARAM_PATTERN.test(level)) {
|
|
607
|
+
throw buildStoryConfigError(
|
|
608
|
+
source,
|
|
609
|
+
`disclosure level "${level}" must be a single lowercase word (letters and digits, no hyphens).`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
if (RESERVED_LEVEL_NAMES.has(level)) {
|
|
613
|
+
throw buildStoryConfigError(
|
|
614
|
+
source,
|
|
615
|
+
`disclosure level "${level}" is a reserved name.`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
rejectReservedObjectKey(
|
|
619
|
+
level,
|
|
620
|
+
`lookup.disclosure.levels[${index}]`,
|
|
621
|
+
source,
|
|
622
|
+
);
|
|
623
|
+
if (seen.has(level)) {
|
|
624
|
+
throw buildStoryConfigError(
|
|
625
|
+
source,
|
|
626
|
+
`duplicate disclosure level "${level}".`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
seen.add(level);
|
|
630
|
+
return level;
|
|
631
|
+
});
|
|
632
|
+
const defaultLevel =
|
|
633
|
+
obj.default === undefined
|
|
634
|
+
? undefined
|
|
635
|
+
: requireString(obj.default, "lookup.disclosure.default", source);
|
|
636
|
+
if (defaultLevel !== undefined && !seen.has(defaultLevel)) {
|
|
637
|
+
throw buildStoryConfigError(
|
|
638
|
+
source,
|
|
639
|
+
`disclosure default "${defaultLevel}" is not one of the declared levels.`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
levels,
|
|
644
|
+
...(defaultLevel !== undefined ? { default: defaultLevel } : {}),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function validateExpandArray(
|
|
649
|
+
raw: unknown,
|
|
650
|
+
where: string,
|
|
651
|
+
source: string,
|
|
652
|
+
): readonly StoryPackExpand[] | undefined {
|
|
653
|
+
if (raw === undefined) return undefined;
|
|
654
|
+
if (!Array.isArray(raw)) {
|
|
655
|
+
throw buildStoryConfigError(source, `"${where}" must be an array.`);
|
|
656
|
+
}
|
|
657
|
+
const names = new Set<string>();
|
|
658
|
+
return raw.map((entry, index) => {
|
|
659
|
+
const expand = validateExpand(entry, `${where}[${index}]`, source);
|
|
660
|
+
if (names.has(expand.name)) {
|
|
661
|
+
throw buildStoryConfigError(
|
|
662
|
+
source,
|
|
663
|
+
`duplicate expand name "${expand.name}" in "${where}".`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
names.add(expand.name);
|
|
667
|
+
return expand;
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function validateExpand(
|
|
672
|
+
raw: unknown,
|
|
673
|
+
where: string,
|
|
674
|
+
source: string,
|
|
675
|
+
): StoryPackExpand {
|
|
676
|
+
const obj = requireObject(raw, where, source);
|
|
677
|
+
const name = requireString(obj.name, `${where}.name`, source);
|
|
678
|
+
if (!FIELD_PATTERN.test(name)) {
|
|
679
|
+
throw buildStoryConfigError(
|
|
680
|
+
source,
|
|
681
|
+
`"${where}.name" must be a simple identifier.`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
rejectReservedObjectKey(name, `${where}.name`, source);
|
|
685
|
+
const relation = requirePredicateTerm(
|
|
686
|
+
obj.relation,
|
|
687
|
+
`${where}.relation`,
|
|
688
|
+
source,
|
|
689
|
+
);
|
|
690
|
+
if (obj.kind !== undefined && obj.kind !== "list" && obj.kind !== "table") {
|
|
691
|
+
throw buildStoryConfigError(
|
|
692
|
+
source,
|
|
693
|
+
`"${where}.kind" must be "list" or "table".`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
if (!Array.isArray(obj.select) || obj.select.length === 0) {
|
|
697
|
+
throw buildStoryConfigError(
|
|
698
|
+
source,
|
|
699
|
+
`"${where}.select" must be a non-empty array.`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
const select: StoryPackExpandField[] = obj.select.map((entry, index) =>
|
|
703
|
+
validateField(entry, `${where}.select[${index}]`, source),
|
|
704
|
+
);
|
|
705
|
+
const heading =
|
|
706
|
+
obj.heading === undefined
|
|
707
|
+
? undefined
|
|
708
|
+
: requireString(obj.heading, `${where}.heading`, source);
|
|
709
|
+
// Membership in the declared levels is cross-checked in validateLookup.
|
|
710
|
+
const level =
|
|
711
|
+
obj.level === undefined
|
|
712
|
+
? undefined
|
|
713
|
+
: requireString(obj.level, `${where}.level`, source);
|
|
714
|
+
return {
|
|
715
|
+
name,
|
|
716
|
+
relation,
|
|
717
|
+
select,
|
|
718
|
+
...(heading !== undefined ? { heading } : {}),
|
|
719
|
+
...(obj.kind !== undefined ? { kind: obj.kind as "list" | "table" } : {}),
|
|
720
|
+
...(obj.showWhenEmpty === true ? { showWhenEmpty: true } : {}),
|
|
721
|
+
...(level !== undefined ? { level } : {}),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function validateFieldArray(
|
|
726
|
+
raw: unknown,
|
|
727
|
+
where: string,
|
|
728
|
+
source: string,
|
|
729
|
+
): readonly StoryPackField[] | undefined {
|
|
730
|
+
if (raw === undefined) return undefined;
|
|
731
|
+
if (!Array.isArray(raw)) {
|
|
732
|
+
throw buildStoryConfigError(source, `"${where}" must be an array.`);
|
|
733
|
+
}
|
|
734
|
+
return raw.map((entry, index) =>
|
|
735
|
+
validateField(entry, `${where}[${index}]`, source),
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function validateSectionArray(
|
|
740
|
+
raw: unknown,
|
|
741
|
+
where: string,
|
|
742
|
+
source: string,
|
|
743
|
+
): readonly StoryPackSection[] | undefined {
|
|
744
|
+
if (raw === undefined) return undefined;
|
|
745
|
+
if (!Array.isArray(raw)) {
|
|
746
|
+
throw buildStoryConfigError(source, `"${where}" must be an array.`);
|
|
747
|
+
}
|
|
748
|
+
return raw.map((entry, index) => {
|
|
749
|
+
const base = validateField(entry, `${where}[${index}]`, source);
|
|
750
|
+
const obj = entry as Record<string, unknown>;
|
|
751
|
+
if (obj.kind !== undefined && obj.kind !== "field" && obj.kind !== "code") {
|
|
752
|
+
throw buildStoryConfigError(
|
|
753
|
+
source,
|
|
754
|
+
`"${where}[${index}].kind" must be "field" or "code".`,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
...base,
|
|
759
|
+
...(obj.kind !== undefined ? { kind: obj.kind as "field" | "code" } : {}),
|
|
760
|
+
};
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function validateField(
|
|
765
|
+
raw: unknown,
|
|
766
|
+
where: string,
|
|
767
|
+
source: string,
|
|
768
|
+
): StoryPackField {
|
|
769
|
+
const obj = requireObject(raw, where, source);
|
|
770
|
+
const name = requireString(obj.name, `${where}.name`, source);
|
|
771
|
+
if (!FIELD_PATTERN.test(name)) {
|
|
772
|
+
throw buildStoryConfigError(
|
|
773
|
+
source,
|
|
774
|
+
`"${where}.name" must be a simple identifier.`,
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
rejectReservedObjectKey(name, `${where}.name`, source);
|
|
778
|
+
const property = requirePredicateTerm(
|
|
779
|
+
obj.property,
|
|
780
|
+
`${where}.property`,
|
|
781
|
+
source,
|
|
782
|
+
);
|
|
783
|
+
const label =
|
|
784
|
+
obj.label === undefined
|
|
785
|
+
? undefined
|
|
786
|
+
: requireString(obj.label, `${where}.label`, source);
|
|
787
|
+
return { name, property, ...(label !== undefined ? { label } : {}) };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function requireObject(
|
|
791
|
+
raw: unknown,
|
|
792
|
+
where: string,
|
|
793
|
+
source: string,
|
|
794
|
+
): Record<string, unknown> {
|
|
795
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
796
|
+
throw buildStoryConfigError(source, `"${where}" must be an object.`);
|
|
797
|
+
}
|
|
798
|
+
return raw as Record<string, unknown>;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function requireString(raw: unknown, where: string, source: string): string {
|
|
802
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
803
|
+
throw buildStoryConfigError(
|
|
804
|
+
source,
|
|
805
|
+
`"${where}" must be a non-empty string.`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
return raw;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Require a prefixed name (`ex:Recipe`) or an embeddable absolute IRI. */
|
|
812
|
+
function requireTerm(raw: unknown, where: string, source: string): string {
|
|
813
|
+
const value = requireString(raw, where, source);
|
|
814
|
+
if (value.includes("://")) {
|
|
815
|
+
if (EMBEDDABLE_IRI_PATTERN.test(value)) return value;
|
|
816
|
+
throw buildStoryConfigError(
|
|
817
|
+
source,
|
|
818
|
+
`"${where}" is not a valid IRI (no whitespace or <>" characters).`,
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
if (PREFIXED_NAME_PATTERN.test(value)) return value;
|
|
822
|
+
throw buildStoryConfigError(
|
|
823
|
+
source,
|
|
824
|
+
`"${where}" must be a prefixed name (ex:Recipe) or an absolute IRI.`,
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Require a predicate term: a single term or a slash-separated SPARQL
|
|
830
|
+
* property path of prefixed names (`cs:hasCategory/cs:slug`).
|
|
831
|
+
*
|
|
832
|
+
* Paths are only meaningful in predicate position (`lookup.by` and
|
|
833
|
+
* `fields[].property`/`sections[].property`); class terms go through
|
|
834
|
+
* `requireTerm` and reject paths.
|
|
835
|
+
*/
|
|
836
|
+
function requirePredicateTerm(
|
|
837
|
+
raw: unknown,
|
|
838
|
+
where: string,
|
|
839
|
+
source: string,
|
|
840
|
+
): string {
|
|
841
|
+
const value = requireString(raw, where, source);
|
|
842
|
+
if (value.includes("://") || !value.includes("/")) {
|
|
843
|
+
return requireTerm(value, where, source);
|
|
844
|
+
}
|
|
845
|
+
const segments = value.split("/");
|
|
846
|
+
for (const segment of segments) {
|
|
847
|
+
if (!PREFIXED_NAME_PATTERN.test(segment)) {
|
|
848
|
+
throw buildStoryConfigError(
|
|
849
|
+
source,
|
|
850
|
+
`"${where}" property path segment "${segment}" must be a prefixed name.`,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return value;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function buildStoryConfigError(source: string, message: string): PragmaError {
|
|
858
|
+
return PragmaError.configError(`Invalid story in ${source}: ${message}`);
|
|
859
|
+
}
|