@canonical/pragma-cli 0.30.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 +64 -35
- 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/parseConfigValues.ts +2 -0
- package/src/config/readConfigLayers.test.ts +2 -0
- package/src/config/readConfigLayers.ts +3 -0
- package/src/config/types.ts +7 -0
- package/src/constants.ts +3 -1
- 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/orchestration/enrichBlocks.ts +6 -3
- package/src/domains/block/stories.ts +9 -3
- package/src/domains/config/commands/channel.test.ts +16 -0
- package/src/domains/config/commands/channel.ts +14 -1
- package/src/domains/config/commands/framework.test.ts +14 -0
- package/src/domains/config/commands/framework.ts +13 -1
- package/src/domains/config/commands/tier.test.ts +7 -0
- package/src/domains/config/commands/tier.ts +14 -1
- package/src/domains/config/operations/validateTier.test.ts +9 -0
- package/src/domains/config/operations/validateTier.ts +10 -2
- 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/checkMcpCommands.ts +9 -6
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
- package/src/domains/doctor/operations/checks.test.ts +6 -4
- 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/operations/inspectUri.test.ts +5 -5
- package/src/domains/graphql/commands/serve.test.ts +12 -6
- package/src/domains/graphql/commands/serve.ts +16 -6
- 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/operations/compileSchema.test.ts +2 -2
- package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
- 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 +5 -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/index.ts +1 -0
- package/src/domains/info/types.ts +13 -0
- package/src/domains/llm/capabilities.test.ts +23 -3
- 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 +49 -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/sample.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +5 -3
- package/src/domains/modifier/stories.ts +9 -0
- package/src/domains/ontology/commands/list.test.ts +5 -3
- package/src/domains/ontology/commands/show.test.ts +85 -9
- 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 +358 -67
- 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/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.ts +53 -8
- package/src/domains/refs/operations/updateRefs.ts +0 -1
- package/src/domains/setup/commands/index.ts +1 -0
- package/src/domains/setup/commands/setup.ts +70 -0
- 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/shared/bootStore.bareCore.test.ts +63 -0
- package/src/domains/shared/bootStore.test.ts +82 -0
- package/src/domains/shared/bootStore.ts +56 -20
- 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.ts +48 -0
- package/src/domains/shared/loaders/gitLoader.ts +1 -2
- package/src/domains/shared/loaders/localLoader.ts +2 -18
- package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
- package/src/domains/shared/loaders/readPackageDir.ts +46 -8
- package/src/domains/shared/lookupMany.test.ts +15 -0
- package/src/domains/shared/lookupMany.ts +22 -6
- package/src/domains/shared/namespaces.ts +13 -5
- 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/resolveLookupExitCode.test.ts +48 -0
- package/src/domains/shared/resolveLookupExitCode.ts +42 -0
- package/src/domains/shared/semanticPackage.test.ts +89 -0
- package/src/domains/shared/semanticPackage.ts +17 -2
- package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
- package/src/domains/shared/stories/compileLookupTool.ts +1 -1
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
- 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 +30 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
- 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 +305 -7
- package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
- package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
- package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
- package/src/domains/shared/stories/pack/index.ts +14 -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/standardParity.test.ts +293 -220
- package/src/domains/shared/stories/pack/types.ts +123 -3
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
- package/src/domains/shared/stories/types.ts +9 -1
- 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/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 +13 -1
- package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
- package/src/domains/trace/commands/show.ts +30 -4
- package/src/index.ts +1 -13
- package/src/mcp/registerResources.test.ts +2 -2
- package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
- package/src/mcp/tools/index.ts +18 -10
- package/src/mcp/tools/registerTools.test.ts +106 -30
- package/src/pipeline/collectCommands.test.ts +151 -1
- package/src/pipeline/collectCommands.ts +34 -19
- 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 +162 -19
- package/src/testing/cli.ts +30 -2
- package/src/testing/dsFixtures.ts +21 -4
- package/src/testing/index.ts +1 -4
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
- package/src/testing/integration/cli-user-stories.test.ts +63 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
- package/src/testing/integration/edge-cases.test.ts +62 -50
- package/src/testing/integration/executor-execution.test.ts +6 -4
- package/src/testing/integration/executor-golden.test.ts +2 -2
- package/src/testing/integration/mcp-surface.test.ts +40 -15
- package/src/testing/integration/parity.test.ts +119 -70
- package/src/testing/setupXdgIsolation.ts +16 -5
- package/src/testing/standardParityFixtures.ts +26 -83
- package/src/testing/store.ts +4 -4
- 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 -11
- package/src/domains/standard/commands/lookup.test.ts +0 -135
- package/src/domains/standard/commands/lookup.ts +0 -14
- 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 -89
- 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 -33
- 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/stories.test.ts +0 -24
- package/src/domains/standard/stories.ts +0 -151
- 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 -9
- 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 -12
- 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/domains/tier/stories.test.ts +0 -22
- package/src/domains/tier/stories.ts +0 -52
|
@@ -1,37 +1,204 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prefix
|
|
2
|
+
* Prefix maps for ke store creation and display compaction.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The core ships only generic RDF vocabularies. Domain prefixes (e.g. the
|
|
5
|
+
* design system's `ds:`/`cs:`) are supplied by the loaded semantic
|
|
6
|
+
* packages and merged over the core at boot via {@link resolvePrefixes}.
|
|
7
|
+
* Renaming a key here updates every derived accessor automatically.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type { PrefixMap } from "@canonical/ke";
|
|
9
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Core prefix map — the generic RDF vocabularies pragma itself ships.
|
|
14
|
+
*
|
|
15
|
+
* Registered as PREFIX declarations in every SPARQL query and used as the
|
|
16
|
+
* base of the effective prefix map. Contains no domain-specific prefixes:
|
|
17
|
+
* `ds:`/`cs:` and any other package namespaces arrive from the resolved
|
|
18
|
+
* semantic packages (see {@link resolvePrefixes}).
|
|
19
|
+
*/
|
|
10
20
|
export const PREFIX_MAP = {
|
|
11
|
-
|
|
12
|
-
cs: "http://pragma.canonical.com/codestandards#",
|
|
21
|
+
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
13
22
|
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
14
23
|
owl: "http://www.w3.org/2002/07/owl#",
|
|
15
24
|
skos: "http://www.w3.org/2004/02/skos/core#",
|
|
25
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
16
26
|
} as const satisfies PrefixMap;
|
|
17
27
|
|
|
18
28
|
/**
|
|
19
|
-
*
|
|
29
|
+
* Transitional design-system prefixes bundled with the core.
|
|
30
|
+
*
|
|
31
|
+
* @remarks transitional — remove in P4. These belong to the
|
|
32
|
+
* `@canonical/design-system` and `@canonical/code-standards` packages and
|
|
33
|
+
* must move into their own prefix declarations. Until those packages
|
|
34
|
+
* declare `pragma.prefixes`, this bundled fallback keeps existing DS
|
|
35
|
+
* queries resolving and DS IRIs compacting in output. Package-declared
|
|
36
|
+
* prefixes (and config overrides) win over this fallback.
|
|
37
|
+
*/
|
|
38
|
+
export const TRANSITIONAL_DS_PREFIX_MAP = {
|
|
39
|
+
ds: "https://ds.canonical.com/",
|
|
40
|
+
cs: "http://pragma.canonical.com/codestandards#",
|
|
41
|
+
} as const satisfies PrefixMap;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The default display/compaction prefix set and the base of the derived
|
|
45
|
+
* accessors — the exact set compacted before P0 split the core from the
|
|
46
|
+
* design system, kept stable during the transition.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately omits `rdf:`/`xsd:` (present in {@link PREFIX_MAP}) so URI
|
|
49
|
+
* compaction in output is unchanged from pre-P0 behavior; the store's
|
|
50
|
+
* query prefix map (see {@link resolvePrefixes}) still carries them.
|
|
51
|
+
*
|
|
52
|
+
* @remarks transitional — the `ds:`/`cs:` members move to their packages
|
|
53
|
+
* in P4, at which point this collapses toward {@link PREFIX_MAP}. Runtime
|
|
54
|
+
* code should prefer the map from {@link resolvePrefixes}, which layers in
|
|
55
|
+
* package-declared and config prefixes over the store base.
|
|
56
|
+
*/
|
|
57
|
+
export const DEFAULT_PREFIX_MAP = {
|
|
58
|
+
ds: TRANSITIONAL_DS_PREFIX_MAP.ds,
|
|
59
|
+
cs: TRANSITIONAL_DS_PREFIX_MAP.cs,
|
|
60
|
+
rdfs: PREFIX_MAP.rdfs,
|
|
61
|
+
owl: PREFIX_MAP.owl,
|
|
62
|
+
skos: PREFIX_MAP.skos,
|
|
63
|
+
} as const satisfies PrefixMap;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Core prefixes reserved for pragma's own vocabularies. Packages and config
|
|
67
|
+
* may not redefine these — every query and the RDF/OWL/SKOS toolchain depend
|
|
68
|
+
* on them resolving to the canonical W3C namespaces, so a package that shadows
|
|
69
|
+
* `rdf:` (accidentally or maliciously) would silently corrupt every query.
|
|
70
|
+
*/
|
|
71
|
+
const RESERVED_PREFIXES: ReadonlySet<string> = new Set(Object.keys(PREFIX_MAP));
|
|
72
|
+
|
|
73
|
+
/** A syntactically safe SPARQL prefix name (PN_PREFIX subset we accept). */
|
|
74
|
+
const SAFE_PREFIX_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reject a namespace that cannot appear inside a SPARQL `<IRIREF>` unescaped.
|
|
78
|
+
* ke injects prefixes verbatim as `PREFIX name: <iri>`; a namespace containing
|
|
79
|
+
* `>` (or whitespace/control/`<"{}|^`\``) would break out of the IRI and inject
|
|
80
|
+
* arbitrary SPARQL into every query. SPARQL's IRIREF grammar forbids exactly
|
|
81
|
+
* these, so anything matching is both unsafe and invalid Turtle/SPARQL.
|
|
82
|
+
*/
|
|
83
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: IRIREF forbids U+0000–U+0020 by grammar
|
|
84
|
+
const UNSAFE_NAMESPACE = /[\x00-\x20<>"{}|^`\\]/;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate and merge one source of declared prefixes onto an accumulator.
|
|
88
|
+
*
|
|
89
|
+
* Skips (with a stderr warning) any entry that is unsafe to inject: a
|
|
90
|
+
* malformed prefix name, a namespace that could break out of the SPARQL
|
|
91
|
+
* IRIREF, or an attempt to redefine a reserved core prefix. Also warns when a
|
|
92
|
+
* later source silently overrides an already-declared prefix (last-wins
|
|
93
|
+
* collision), so a name clash between two packages is visible rather than
|
|
94
|
+
* mysterious.
|
|
95
|
+
*
|
|
96
|
+
* @note Impure — writes warnings to stderr.
|
|
97
|
+
*/
|
|
98
|
+
function mergePrefixSource(
|
|
99
|
+
acc: Record<string, string>,
|
|
100
|
+
claimed: Set<string>,
|
|
101
|
+
source: Readonly<Record<string, string>> | undefined,
|
|
102
|
+
origin: string,
|
|
103
|
+
): void {
|
|
104
|
+
if (!source) return;
|
|
105
|
+
for (const [name, namespace] of Object.entries(source)) {
|
|
106
|
+
if (!SAFE_PREFIX_NAME.test(name)) {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
`Warning: ignoring prefix "${name}" from ${origin} — not a valid prefix name.\n`,
|
|
109
|
+
);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (RESERVED_PREFIXES.has(name)) {
|
|
113
|
+
process.stderr.write(
|
|
114
|
+
`Warning: ignoring prefix "${name}" from ${origin} — "${name}:" is reserved for a core RDF vocabulary and cannot be redefined.\n`,
|
|
115
|
+
);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (UNSAFE_NAMESPACE.test(namespace)) {
|
|
119
|
+
process.stderr.write(
|
|
120
|
+
`Warning: ignoring prefix "${name}" from ${origin} — namespace contains characters that are not valid in an IRI.\n`,
|
|
121
|
+
);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Warn only on a collision between two *validated* sources (package vs.
|
|
125
|
+
// package/config). Silently overriding the trusted DS fallback is the
|
|
126
|
+
// expected transitional handoff, not a clash.
|
|
127
|
+
if (claimed.has(name) && acc[name] !== namespace) {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
`Warning: prefix "${name}" from ${origin} overrides an earlier declaration (was <${acc[name]}>, now <${namespace}>).\n`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
acc[name] = namespace;
|
|
133
|
+
claimed.add(name);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve the effective prefix map for a boot.
|
|
139
|
+
*
|
|
140
|
+
* Precedence (lowest → highest): the generic {@link PREFIX_MAP}, the
|
|
141
|
+
* transitional DS fallback, prefixes declared by resolved packages (in
|
|
142
|
+
* array order), then config-level overrides. A package that declares its
|
|
143
|
+
* own `ds:`/`cs:` therefore overrides the bundled fallback, and a user's
|
|
144
|
+
* `pragma.config.json` `prefixes` wins over everything.
|
|
145
|
+
*
|
|
146
|
+
* Package- and config-declared prefixes are validated before they reach the
|
|
147
|
+
* store: unsafe names/namespaces are skipped (they would otherwise be injected
|
|
148
|
+
* verbatim into every SPARQL query), reserved core prefixes cannot be
|
|
149
|
+
* redefined, and last-wins collisions are surfaced as warnings. The bundled
|
|
150
|
+
* core and DS fallback are trusted and merged as-is.
|
|
151
|
+
*
|
|
152
|
+
* @param packages - Resolved packages, each optionally declaring prefixes.
|
|
153
|
+
* @param configPrefixes - Config-level prefix overrides.
|
|
154
|
+
* @returns The merged prefix map for the store and every query.
|
|
155
|
+
*/
|
|
156
|
+
export function resolvePrefixes(
|
|
157
|
+
packages: readonly {
|
|
158
|
+
readonly name?: string;
|
|
159
|
+
readonly prefixes?: Readonly<Record<string, string>>;
|
|
160
|
+
}[] = [],
|
|
161
|
+
configPrefixes?: Readonly<Record<string, string>>,
|
|
162
|
+
): Record<string, string> {
|
|
163
|
+
const merged: Record<string, string> = {
|
|
164
|
+
...PREFIX_MAP,
|
|
165
|
+
...TRANSITIONAL_DS_PREFIX_MAP,
|
|
166
|
+
};
|
|
167
|
+
// Keys claimed by a validated source, so collisions between two such
|
|
168
|
+
// sources warn while overriding the trusted seed stays silent.
|
|
169
|
+
const claimed = new Set<string>();
|
|
170
|
+
for (const pkg of packages) {
|
|
171
|
+
// Name the declaring package in the origin so collision/injection
|
|
172
|
+
// warnings point at the actual source, not a generic placeholder.
|
|
173
|
+
const origin = pkg.name ? `package "${pkg.name}"` : "a semantic package";
|
|
174
|
+
mergePrefixSource(merged, claimed, pkg.prefixes, origin);
|
|
175
|
+
}
|
|
176
|
+
mergePrefixSource(merged, claimed, configPrefixes, "pragma.config.json");
|
|
177
|
+
return merged;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* SPARQL prefix accessors derived from {@link DEFAULT_PREFIX_MAP}.
|
|
20
182
|
*
|
|
21
183
|
* Use in query templates: `${P.ds}Component` → `"ds:Component"`.
|
|
22
|
-
* Renaming a key in
|
|
184
|
+
* Renaming a key in the underlying maps automatically updates every query.
|
|
185
|
+
*
|
|
186
|
+
* @remarks The `ds:`/`cs:` accessors are transitional (see
|
|
187
|
+
* {@link TRANSITIONAL_DS_PREFIX_MAP}); they resolve at query time against
|
|
188
|
+
* the store's merged prefix map, which the packages now supply.
|
|
23
189
|
*/
|
|
24
190
|
export const P = Object.fromEntries(
|
|
25
|
-
Object.keys(
|
|
26
|
-
) as { readonly [K in keyof typeof
|
|
191
|
+
Object.keys(DEFAULT_PREFIX_MAP).map((k) => [k, `${k}:`]),
|
|
192
|
+
) as { readonly [K in keyof typeof DEFAULT_PREFIX_MAP]: `${K}:` };
|
|
27
193
|
|
|
28
194
|
/**
|
|
29
|
-
* Turtle `@prefix` declarations derived from {@link
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* don't need (ke auto-injects PREFIX_MAP for queries).
|
|
195
|
+
* Turtle `@prefix` declarations derived from {@link DEFAULT_PREFIX_MAP},
|
|
196
|
+
* plus `xsd:` — commonly used by TTL data files but omitted from the
|
|
197
|
+
* display set (SPARQL queries auto-inject the store prefix map).
|
|
33
198
|
*/
|
|
34
199
|
export const TTL_PREFIXES = [
|
|
35
|
-
...Object.entries(
|
|
36
|
-
|
|
200
|
+
...Object.entries(DEFAULT_PREFIX_MAP).map(
|
|
201
|
+
([k, uri]) => `@prefix ${k}: <${uri}> .`,
|
|
202
|
+
),
|
|
203
|
+
`@prefix xsd: <${PREFIX_MAP.xsd}> .`,
|
|
37
204
|
].join("\n");
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
RenderLookupOptions,
|
|
14
14
|
SectionDef,
|
|
15
15
|
} from "./contracts.js";
|
|
16
|
-
import {
|
|
16
|
+
import { DEFAULT_PREFIX_MAP } from "./prefixes.js";
|
|
17
17
|
|
|
18
18
|
type RenderMode = "plain" | "llm";
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@ export function renderListPlain<T>(
|
|
|
21
21
|
items: readonly T[],
|
|
22
22
|
options: RenderListOptions<T>,
|
|
23
23
|
): string {
|
|
24
|
-
const prefixes = options.prefixes ??
|
|
24
|
+
const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
|
|
25
25
|
const rows = items.map((item) =>
|
|
26
26
|
options.columns
|
|
27
27
|
.map((column) => ({
|
|
@@ -52,7 +52,7 @@ export function renderListLlm<T>(
|
|
|
52
52
|
items: readonly T[],
|
|
53
53
|
options: RenderListOptions<T>,
|
|
54
54
|
): string {
|
|
55
|
-
const prefixes = options.prefixes ??
|
|
55
|
+
const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
|
|
56
56
|
const lines = [`## ${options.heading} (${items.length})`, ""];
|
|
57
57
|
|
|
58
58
|
for (const item of items) {
|
|
@@ -121,7 +121,7 @@ function renderLookupFields<T>(
|
|
|
121
121
|
options: RenderLookupOptions<T>,
|
|
122
122
|
mode: RenderMode,
|
|
123
123
|
): string[] {
|
|
124
|
-
const prefixes = options.prefixes ??
|
|
124
|
+
const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
|
|
125
125
|
return options.fields.flatMap((field) => {
|
|
126
126
|
const value = field.value(entity);
|
|
127
127
|
if (isEmptyValue(value)) {
|
|
@@ -173,7 +173,7 @@ function renderSectionValue<T>(
|
|
|
173
173
|
options: RenderLookupOptions<T>,
|
|
174
174
|
mode: RenderMode,
|
|
175
175
|
): string | null {
|
|
176
|
-
const prefixes = options.prefixes ??
|
|
176
|
+
const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
|
|
177
177
|
|
|
178
178
|
switch (section.kind) {
|
|
179
179
|
case "field":
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { LookupResult } from "./contracts.js";
|
|
3
|
+
import resolveLookupExitCode from "./resolveLookupExitCode.js";
|
|
4
|
+
|
|
5
|
+
function makeResult<T>(
|
|
6
|
+
results: T[],
|
|
7
|
+
errors: LookupResult<T>["errors"],
|
|
8
|
+
): LookupResult<T> {
|
|
9
|
+
return { results, errors };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("resolveLookupExitCode", () => {
|
|
13
|
+
it("returns 0 when at least one result was found", () => {
|
|
14
|
+
const result = makeResult(
|
|
15
|
+
[{ name: "ok" }],
|
|
16
|
+
[{ query: "miss", code: "ENTITY_NOT_FOUND", message: "not found" }],
|
|
17
|
+
);
|
|
18
|
+
expect(resolveLookupExitCode(result)).toBe(0);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns 0 when there are no errors", () => {
|
|
22
|
+
expect(resolveLookupExitCode(makeResult([{ name: "ok" }], []))).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("maps an all-not-found lookup to exit 1", () => {
|
|
26
|
+
const result = makeResult(
|
|
27
|
+
[],
|
|
28
|
+
[{ query: "x", code: "ENTITY_NOT_FOUND", message: "not found" }],
|
|
29
|
+
);
|
|
30
|
+
expect(resolveLookupExitCode(result)).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("maps an all-invalid-input lookup to exit 3", () => {
|
|
34
|
+
const result = makeResult(
|
|
35
|
+
[],
|
|
36
|
+
[{ query: "x", code: "INVALID_INPUT", message: "bad" }],
|
|
37
|
+
);
|
|
38
|
+
expect(resolveLookupExitCode(result)).toBe(3);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("falls back to exit 1 for an unrecognised error code", () => {
|
|
42
|
+
const result = makeResult(
|
|
43
|
+
[],
|
|
44
|
+
[{ query: "x", code: "NOT_A_REAL_CODE", message: "?" }],
|
|
45
|
+
);
|
|
46
|
+
expect(resolveLookupExitCode(result)).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute the process exit code for a lookup result.
|
|
3
|
+
*
|
|
4
|
+
* A lookup that produced at least one result — even alongside some failures —
|
|
5
|
+
* is a partial success and exits 0. A lookup where every query failed exits
|
|
6
|
+
* non-zero, mapping the first error's code to its exit code so callers and
|
|
7
|
+
* scripts can detect the failure. The errors are still rendered inline for the
|
|
8
|
+
* human reader.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ERROR_CODES, type ErrorCode } from "#error";
|
|
12
|
+
// mapExitCode is imported by its direct path, not the #pipeline barrel: the
|
|
13
|
+
// barrel re-exports runCli/createProgram, which would form an import cycle
|
|
14
|
+
// through this shared utility and break module initialization.
|
|
15
|
+
import mapExitCode from "../../pipeline/mapExitCode.js";
|
|
16
|
+
import type { LookupResult } from "./contracts.js";
|
|
17
|
+
|
|
18
|
+
/** Default non-zero exit when a lookup error carries an unrecognised code. */
|
|
19
|
+
const FALLBACK_LOOKUP_EXIT_CODE = 1;
|
|
20
|
+
|
|
21
|
+
/** Narrow an arbitrary error-code string to a known {@link ErrorCode}. */
|
|
22
|
+
function isErrorCode(code: string | undefined): code is ErrorCode {
|
|
23
|
+
return (
|
|
24
|
+
code !== undefined && (ERROR_CODES as readonly string[]).includes(code)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the exit code a lookup command should report.
|
|
30
|
+
*
|
|
31
|
+
* @param result - The resolved lookup result.
|
|
32
|
+
* @returns 0 on success/partial success, else the mapped non-zero exit code.
|
|
33
|
+
*/
|
|
34
|
+
export default function resolveLookupExitCode<T>(
|
|
35
|
+
result: LookupResult<T>,
|
|
36
|
+
): number {
|
|
37
|
+
if (result.results.length > 0 || result.errors.length === 0) return 0;
|
|
38
|
+
const firstCode = result.errors.at(0)?.code;
|
|
39
|
+
return isErrorCode(firstCode)
|
|
40
|
+
? mapExitCode(firstCode)
|
|
41
|
+
: FALLBACK_LOOKUP_EXIT_CODE;
|
|
42
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { PackageRef } from "../refs/operations/parseRef.js";
|
|
3
|
+
import type { PackageLoader, SemanticPackage } from "./semanticPackage.js";
|
|
4
|
+
import { resolveSemanticPackages } from "./semanticPackage.js";
|
|
5
|
+
|
|
6
|
+
function makeRef(pkg: string): PackageRef {
|
|
7
|
+
return { kind: "npm", pkg };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function makePackage(name: string): SemanticPackage {
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
source: "local",
|
|
15
|
+
graphs: [{ path: `${name}/data.ttl`, content: "", format: "turtle" }],
|
|
16
|
+
skills: [],
|
|
17
|
+
stories: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("resolveSemanticPackages", () => {
|
|
22
|
+
it("resolves each ref through the loader chain", async () => {
|
|
23
|
+
const loader: PackageLoader = {
|
|
24
|
+
name: "local",
|
|
25
|
+
resolve: (ref) => makePackage(ref.pkg),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const packages = await resolveSemanticPackages(
|
|
29
|
+
[makeRef("a"), makeRef("b")],
|
|
30
|
+
[loader],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(packages.map((pkg) => pkg.name)).toEqual(["a", "b"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("drops refs no loader can resolve", async () => {
|
|
37
|
+
const loader: PackageLoader = {
|
|
38
|
+
name: "local",
|
|
39
|
+
resolve: (ref) => (ref.pkg === "a" ? makePackage("a") : undefined),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const packages = await resolveSemanticPackages(
|
|
43
|
+
[makeRef("a"), makeRef("b")],
|
|
44
|
+
[loader],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(packages.map((pkg) => pkg.name)).toEqual(["a"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Regression: the bundled loader returns ONE cached aggregate package for
|
|
51
|
+
// every ref it serves. Including it once per ref loaded its graphs into
|
|
52
|
+
// the store multiple times, re-minting blank nodes on each parse — which
|
|
53
|
+
// triplicated blank-node subgraphs (standard do/don't examples) whenever
|
|
54
|
+
// all three default refs fell back to the bundled data.
|
|
55
|
+
it("includes a package instance resolved for multiple refs only once", async () => {
|
|
56
|
+
const aggregate = makePackage("(bundled)");
|
|
57
|
+
const loader: PackageLoader = {
|
|
58
|
+
name: "bundled",
|
|
59
|
+
resolve: () => aggregate,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const packages = await resolveSemanticPackages(
|
|
63
|
+
[makeRef("a"), makeRef("b"), makeRef("c")],
|
|
64
|
+
[loader],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(packages).toHaveLength(1);
|
|
68
|
+
expect(packages[0]).toBe(aggregate);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("keeps distinct packages from a mixed loader chain", async () => {
|
|
72
|
+
const aggregate = makePackage("(bundled)");
|
|
73
|
+
const git: PackageLoader = {
|
|
74
|
+
name: "git",
|
|
75
|
+
resolve: (ref) => (ref.pkg === "a" ? makePackage("a") : undefined),
|
|
76
|
+
};
|
|
77
|
+
const bundled: PackageLoader = {
|
|
78
|
+
name: "bundled",
|
|
79
|
+
resolve: () => aggregate,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const packages = await resolveSemanticPackages(
|
|
83
|
+
[makeRef("a"), makeRef("b"), makeRef("c")],
|
|
84
|
+
[git, bundled],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(packages.map((pkg) => pkg.name)).toEqual(["a", "(bundled)"]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -53,6 +53,13 @@ export interface SemanticPackage {
|
|
|
53
53
|
readonly source: "local" | "git" | "bundled";
|
|
54
54
|
/** TTL graph content to load into the ke store. */
|
|
55
55
|
readonly graphs: GraphContent[];
|
|
56
|
+
/**
|
|
57
|
+
* Namespace prefixes this package declares, merged over the core prefix
|
|
58
|
+
* map at boot (see {@link resolvePrefixes}). Read from the package's
|
|
59
|
+
* `package.json` `pragma.prefixes` field. Absent when the package
|
|
60
|
+
* declares none.
|
|
61
|
+
*/
|
|
62
|
+
readonly prefixes?: Readonly<Record<string, string>>;
|
|
56
63
|
/** Skills available in this package. */
|
|
57
64
|
readonly skills: SkillEntry[];
|
|
58
65
|
/**
|
|
@@ -87,15 +94,22 @@ export interface PackageLoader {
|
|
|
87
94
|
* returns a result wins — subsequent loaders are skipped for that ref.
|
|
88
95
|
* Refs that no loader can resolve are silently dropped.
|
|
89
96
|
*
|
|
97
|
+
* A package instance that resolves for more than one ref (the bundled
|
|
98
|
+
* loader returns one cached aggregate for every ref it serves) is
|
|
99
|
+
* included only once: loading the same graphs repeatedly re-mints their
|
|
100
|
+
* blank nodes on each parse, duplicating blank-node subgraphs (e.g.
|
|
101
|
+
* standard do/don't examples) in the store.
|
|
102
|
+
*
|
|
90
103
|
* @param refs - Parsed package references from config merge.
|
|
91
104
|
* @param loaders - Loaders in precedence order (local > git > bundled).
|
|
92
|
-
* @returns Array of resolved semantic packages.
|
|
105
|
+
* @returns Array of resolved semantic packages, deduplicated by identity.
|
|
93
106
|
*/
|
|
94
107
|
export async function resolveSemanticPackages(
|
|
95
108
|
refs: ReadonlyArray<PackageRef>,
|
|
96
109
|
loaders: ReadonlyArray<PackageLoader>,
|
|
97
110
|
): Promise<SemanticPackage[]> {
|
|
98
111
|
const packages: SemanticPackage[] = [];
|
|
112
|
+
const seen = new Set<SemanticPackage>();
|
|
99
113
|
|
|
100
114
|
for (const ref of refs) {
|
|
101
115
|
let resolved: SemanticPackage | undefined;
|
|
@@ -103,7 +117,8 @@ export async function resolveSemanticPackages(
|
|
|
103
117
|
resolved = await loader.resolve(ref);
|
|
104
118
|
if (resolved) break;
|
|
105
119
|
}
|
|
106
|
-
if (resolved) {
|
|
120
|
+
if (resolved && !seen.has(resolved)) {
|
|
121
|
+
seen.add(resolved);
|
|
107
122
|
packages.push(resolved);
|
|
108
123
|
}
|
|
109
124
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import type { PragmaContext } from "../context.js";
|
|
8
8
|
import type { LookupResult } from "../contracts.js";
|
|
9
9
|
import { renderLookupResults } from "../formatters.js";
|
|
10
|
+
import resolveLookupExitCode from "../resolveLookupExitCode.js";
|
|
10
11
|
import normalizeNames from "./normalizeNames.js";
|
|
11
12
|
import projectCliParameters from "./projectCliParameters.js";
|
|
12
13
|
import resolveLookupDetailed from "./resolveLookupDetailed.js";
|
|
@@ -50,7 +51,7 @@ export default function compileLookupCommand<TDetailed, TFmtInput>(
|
|
|
50
51
|
detailed: resolveLookupDetailed(story, "cli", params),
|
|
51
52
|
params,
|
|
52
53
|
};
|
|
53
|
-
const result = await story.resolve(ctx, names, params);
|
|
54
|
+
const result = await story.resolve(ctx, names, params, view);
|
|
54
55
|
const renderInk = story.renderInk;
|
|
55
56
|
|
|
56
57
|
return createOutputResult<LookupOutput>(
|
|
@@ -68,6 +69,7 @@ export default function compileLookupCommand<TDetailed, TFmtInput>(
|
|
|
68
69
|
renderInk(output.result, output.view),
|
|
69
70
|
}),
|
|
70
71
|
},
|
|
72
|
+
resolveLookupExitCode(result),
|
|
71
73
|
);
|
|
72
74
|
},
|
|
73
75
|
};
|
|
@@ -29,7 +29,7 @@ export default function compileLookupTool<TDetailed, TFmtInput>(
|
|
|
29
29
|
detailed: resolveLookupDetailed(story, "mcp", params),
|
|
30
30
|
params,
|
|
31
31
|
};
|
|
32
|
-
const result = await story.resolve(rt, names, params);
|
|
32
|
+
const result = await story.resolve(rt, names, params, view);
|
|
33
33
|
|
|
34
34
|
if (params.condensed) {
|
|
35
35
|
const textParts = result.results.map((entity) =>
|
|
@@ -100,3 +100,52 @@ describe("applyPackFilters", () => {
|
|
|
100
100
|
expect(rows).toEqual([]);
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
|
+
|
|
104
|
+
// Value-free filters (pack v1): no declared value set, case-insensitive
|
|
105
|
+
// equality against the variable — the valid values live in the data.
|
|
106
|
+
describe("applyPackFilters — value-free filters", () => {
|
|
107
|
+
const FREE_FILTERS: StoryPackFilter[] = [
|
|
108
|
+
{ param: "category", variable: "category" },
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
it("matches on case-insensitive equality with the variable", () => {
|
|
112
|
+
const rows = applyPackFilters(ROWS, FREE_FILTERS, { category: "SOUP" });
|
|
113
|
+
expect(rows.map((row) => row.name)).toEqual(["Gazpacho"]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("trims surrounding whitespace from the provided value", () => {
|
|
117
|
+
const rows = applyPackFilters(ROWS, FREE_FILTERS, { category: " soup " });
|
|
118
|
+
expect(rows.map((row) => row.name)).toEqual(["Gazpacho"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns no rows for an unknown value instead of rejecting", () => {
|
|
122
|
+
expect(
|
|
123
|
+
applyPackFilters(ROWS, FREE_FILTERS, { category: "dinner" }),
|
|
124
|
+
).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("matches unicode values across NFC/NFD normalization forms", () => {
|
|
128
|
+
const rows = [{ name: "Salad", kind: "entr\u00e9e" }];
|
|
129
|
+
const filters: StoryPackFilter[] = [{ param: "kind", variable: "kind" }];
|
|
130
|
+
expect(
|
|
131
|
+
applyPackFilters(rows, filters, { kind: "entre\u0301e" }),
|
|
132
|
+
).toHaveLength(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("never matches rows without a binding for the variable", () => {
|
|
136
|
+
const rows = [{ name: "Mystery" }, ...ROWS];
|
|
137
|
+
const filtered = applyPackFilters(rows, FREE_FILTERS, { category: "soup" });
|
|
138
|
+
expect(filtered.map((row) => row.name)).toEqual(["Gazpacho"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("rejects non-string values with INVALID_INPUT", () => {
|
|
142
|
+
expect(() =>
|
|
143
|
+
applyPackFilters(ROWS, FREE_FILTERS, { category: 3 }),
|
|
144
|
+
).toThrowError(PragmaError);
|
|
145
|
+
try {
|
|
146
|
+
applyPackFilters(ROWS, FREE_FILTERS, { category: ["soup"] });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
expect((error as PragmaError).code).toBe("INVALID_INPUT");
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|