@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
|
@@ -36,9 +36,66 @@ export interface StoryPackSection extends StoryPackField {
|
|
|
36
36
|
readonly kind?: "field" | "code";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** A field read from each child node of an {@link StoryPackExpand}. */
|
|
40
|
+
export interface StoryPackExpandField {
|
|
41
|
+
/** Output field name on the child record. */
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/** Property to read on the child node — prefixed name, IRI, or path. */
|
|
44
|
+
readonly property: string;
|
|
45
|
+
/** Display label (defaults to the field name). */
|
|
46
|
+
readonly label?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A multi-valued nested projection: `entity → relation → child nodes`, each
|
|
51
|
+
* child projected to a small record. This is pack v1's structured-section
|
|
52
|
+
* primitive — the one thing v0 could not express (dos/donts, token scales,
|
|
53
|
+
* modifier values). Resolved with a generated, injection-safe sub-SELECT bound
|
|
54
|
+
* to the already-resolved entity IRI (never user input), so it needs no
|
|
55
|
+
* GraphQL. Rendered through the shared `list`/`table` render-kinds.
|
|
56
|
+
*/
|
|
57
|
+
export interface StoryPackExpand {
|
|
58
|
+
/** Output field holding the child array on the looked-up entity. */
|
|
59
|
+
readonly name: string;
|
|
60
|
+
/** Section heading (defaults to the field name). */
|
|
61
|
+
readonly heading?: string;
|
|
62
|
+
/** Render kind for the array (v1: `list` or `table`; defaults to `list`). */
|
|
63
|
+
readonly kind?: "list" | "table";
|
|
64
|
+
/** Relation from the entity to each child node — prefixed name, IRI, or path. */
|
|
65
|
+
readonly relation: string;
|
|
66
|
+
/** Fields to read from each child node. */
|
|
67
|
+
readonly select: readonly StoryPackExpandField[];
|
|
68
|
+
/** Render the section even when the array is empty (default: false). */
|
|
69
|
+
readonly showWhenEmpty?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Minimum disclosure level at which this expand is fetched and rendered — a
|
|
72
|
+
* name from {@link StoryPackDisclosure.levels}. Omitted means the base level
|
|
73
|
+
* (always included). Below its level the sub-SELECT never runs, so gating is
|
|
74
|
+
* both a cost and a rendering control.
|
|
75
|
+
*/
|
|
76
|
+
readonly level?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Progressive-disclosure capability for a lookup.
|
|
81
|
+
*
|
|
82
|
+
* Declares an **ordered** list of named levels (the first is the base/default,
|
|
83
|
+
* always shown). Fields/expands tag themselves with the minimum level at which
|
|
84
|
+
* they appear. The compiler derives from this one declaration: a `--detail
|
|
85
|
+
* <level>` CLI flag, a `detail` MCP parameter (enumerated from the levels), and
|
|
86
|
+
* the JSON/plain/llm projection for the selected level. Levels are additive and
|
|
87
|
+
* optional — a lookup with no disclosure exposes no detail flag.
|
|
88
|
+
*/
|
|
89
|
+
export interface StoryPackDisclosure {
|
|
90
|
+
/** Ordered level names, lowest → highest. The first is the base/default. */
|
|
91
|
+
readonly levels: readonly string[];
|
|
92
|
+
/** Default level when none is requested (must be one of `levels`). */
|
|
93
|
+
readonly default?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
39
96
|
/**
|
|
40
97
|
* A declarative list filter: a CLI/MCP parameter constraining one SELECT
|
|
41
|
-
* variable
|
|
98
|
+
* variable.
|
|
42
99
|
*
|
|
43
100
|
* Filters are row predicates applied AFTER the author query runs — the
|
|
44
101
|
* query text is never modified, so filter input cannot inject SPARQL,
|
|
@@ -52,8 +109,27 @@ export interface StoryPackFilter {
|
|
|
52
109
|
readonly param: string;
|
|
53
110
|
/** SELECT variable the filter constrains (without `?`). */
|
|
54
111
|
readonly variable: string;
|
|
55
|
-
/**
|
|
56
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Allowed values; anything else is rejected with INVALID_INPUT and the
|
|
114
|
+
* set projects to CLI select choices and an MCP enum. Omitted when the
|
|
115
|
+
* value set is data-driven (e.g. categories that live in the graph): the
|
|
116
|
+
* parameter is then a free string matched case-insensitively against the
|
|
117
|
+
* variable — still a post-query row predicate, never query text.
|
|
118
|
+
*/
|
|
119
|
+
readonly values?: readonly string[];
|
|
120
|
+
/** Help text (defaults to a generated description). */
|
|
121
|
+
readonly description?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Free-text search over list rows: a `--search` string parameter that
|
|
126
|
+
* keeps a row when ANY named SELECT variable's value contains the term
|
|
127
|
+
* (case-insensitive substring). Like filters, search is applied after the
|
|
128
|
+
* author query runs — user input never touches the query text.
|
|
129
|
+
*/
|
|
130
|
+
export interface StoryPackSearch {
|
|
131
|
+
/** SELECT variables searched (without `?`). */
|
|
132
|
+
readonly variables: readonly string[];
|
|
57
133
|
/** Help text (defaults to a generated description). */
|
|
58
134
|
readonly description?: string;
|
|
59
135
|
}
|
|
@@ -66,6 +142,38 @@ export interface StoryPackList {
|
|
|
66
142
|
readonly columns: readonly StoryPackColumn[];
|
|
67
143
|
/** Declarative filters projected to CLI flags and MCP parameters. */
|
|
68
144
|
readonly filters?: readonly StoryPackFilter[];
|
|
145
|
+
/** Free-text search projected to a `--search` flag / `search` parameter. */
|
|
146
|
+
readonly search?: StoryPackSearch;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* An additional list-shaped verb: `<noun> <verb>` and `<noun>_<verb>`
|
|
151
|
+
* compiled through the SAME machinery as `list` (query, columns, filters,
|
|
152
|
+
* search) — e.g. standard's `categories`. The verb may not collide with
|
|
153
|
+
* the compiled `list`/`lookup`/`sample` verbs.
|
|
154
|
+
*/
|
|
155
|
+
export interface StoryPackVerb extends StoryPackList {
|
|
156
|
+
/** Verb name (kebab-case), e.g. `"categories"`. */
|
|
157
|
+
readonly verb: string;
|
|
158
|
+
/** CLI description for the command. */
|
|
159
|
+
readonly description?: string;
|
|
160
|
+
/** MCP tool description (defaults to the CLI description). */
|
|
161
|
+
readonly toolDescription?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The sample capability: `<noun> sample [count]` and `<noun>_sample`
|
|
166
|
+
* return 1–5 randomly selected complete entities (resolved through the
|
|
167
|
+
* lookup path at the HIGHEST disclosure level) so agents can learn the
|
|
168
|
+
* data shape before querying.
|
|
169
|
+
*/
|
|
170
|
+
export interface StoryPackSample {
|
|
171
|
+
/** Default sample count when none is requested (1–5; default 2). */
|
|
172
|
+
readonly count?: number;
|
|
173
|
+
/** CLI description for the command. */
|
|
174
|
+
readonly description?: string;
|
|
175
|
+
/** MCP tool description (defaults to the CLI description). */
|
|
176
|
+
readonly toolDescription?: string;
|
|
69
177
|
}
|
|
70
178
|
|
|
71
179
|
/**
|
|
@@ -78,10 +186,20 @@ export interface StoryPackLookup {
|
|
|
78
186
|
readonly by: string;
|
|
79
187
|
/** Optional class constraint — prefixed name or IRI. */
|
|
80
188
|
readonly type?: string;
|
|
189
|
+
/** CLI description for the lookup command (defaults to a generated one). */
|
|
190
|
+
readonly description?: string;
|
|
191
|
+
/** MCP tool description (defaults to a generated one). */
|
|
192
|
+
readonly toolDescription?: string;
|
|
81
193
|
/** Inline fields shown under the entity title. */
|
|
82
194
|
readonly fields?: readonly StoryPackField[];
|
|
83
195
|
/** Long-form sections shown after the fields. */
|
|
84
196
|
readonly sections?: readonly StoryPackSection[];
|
|
197
|
+
/** Multi-valued nested projections (pack v1 structured sections). */
|
|
198
|
+
readonly expand?: readonly StoryPackExpand[];
|
|
199
|
+
/** Progressive-disclosure levels; enables the derived `--detail` flag. */
|
|
200
|
+
readonly disclosure?: StoryPackDisclosure;
|
|
201
|
+
/** Sample capability: `true` for defaults, or a configured sample. */
|
|
202
|
+
readonly sample?: true | StoryPackSample;
|
|
85
203
|
}
|
|
86
204
|
|
|
87
205
|
/** One declarative read story: a noun with its preferred queries. */
|
|
@@ -93,6 +211,8 @@ export interface StoryPackDefinition {
|
|
|
93
211
|
/** MCP tool description (defaults to the CLI description). */
|
|
94
212
|
readonly toolDescription?: string;
|
|
95
213
|
readonly list: StoryPackList;
|
|
214
|
+
/** Additional list-shaped verbs beyond `list` (e.g. `categories`). */
|
|
215
|
+
readonly verbs?: readonly StoryPackVerb[];
|
|
96
216
|
readonly lookup?: StoryPackLookup;
|
|
97
217
|
}
|
|
98
218
|
|
|
@@ -237,4 +237,506 @@ describe("validateStoryPackDefinition — list filters", () => {
|
|
|
237
237
|
),
|
|
238
238
|
).toThrow(/values/);
|
|
239
239
|
});
|
|
240
|
+
|
|
241
|
+
it("accepts a value-free filter (data-driven value set)", () => {
|
|
242
|
+
const validated = validateStoryPackDefinition(
|
|
243
|
+
withFilters([{ param: "category", variable: "category" }]),
|
|
244
|
+
"test",
|
|
245
|
+
);
|
|
246
|
+
expect(validated.list.filters?.at(0)).toEqual({
|
|
247
|
+
param: "category",
|
|
248
|
+
variable: "category",
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("rejects the compiled search parameter name", () => {
|
|
253
|
+
expect(() =>
|
|
254
|
+
validateStoryPackDefinition(
|
|
255
|
+
withFilters([{ param: "search", variable: "category", values: ["x"] }]),
|
|
256
|
+
"test",
|
|
257
|
+
),
|
|
258
|
+
).toThrow(/reserved/);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("rejects the derived detail parameter name", () => {
|
|
262
|
+
expect(() =>
|
|
263
|
+
validateStoryPackDefinition(
|
|
264
|
+
withFilters([{ param: "detail", variable: "category", values: ["x"] }]),
|
|
265
|
+
"test",
|
|
266
|
+
),
|
|
267
|
+
).toThrow(/reserved/);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/** RECIPE_STORY with the given `list.search`. */
|
|
271
|
+
function withSearch(search: unknown): unknown {
|
|
272
|
+
const list = { ...(base.list as Record<string, unknown>), search };
|
|
273
|
+
return { ...base, list };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
it("accepts a search over variables the query mentions", () => {
|
|
277
|
+
const validated = validateStoryPackDefinition(
|
|
278
|
+
withSearch({ variables: ["name", "category"] }),
|
|
279
|
+
"test",
|
|
280
|
+
);
|
|
281
|
+
expect(validated.list.search?.variables).toEqual(["name", "category"]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("rejects a search without variables", () => {
|
|
285
|
+
expect(() =>
|
|
286
|
+
validateStoryPackDefinition(withSearch({ variables: [] }), "test"),
|
|
287
|
+
).toThrow(/search.variables/);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("rejects a search variable the query never mentions", () => {
|
|
291
|
+
expect(() =>
|
|
292
|
+
validateStoryPackDefinition(
|
|
293
|
+
withSearch({ variables: ["season"] }),
|
|
294
|
+
"test",
|
|
295
|
+
),
|
|
296
|
+
).toThrow(/does not appear in "list.query"/);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
/** RECIPE_STORY with the given `lookup.expand`, for expand validation. */
|
|
300
|
+
function withExpand(expand: unknown): unknown {
|
|
301
|
+
return {
|
|
302
|
+
...RECIPE_STORY,
|
|
303
|
+
lookup: { ...RECIPE_STORY.lookup, expand },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
it("accepts a valid expand", () => {
|
|
308
|
+
const validated = validateStoryPackDefinition(
|
|
309
|
+
withExpand([
|
|
310
|
+
{
|
|
311
|
+
name: "ingredients",
|
|
312
|
+
heading: "Ingredients",
|
|
313
|
+
kind: "table",
|
|
314
|
+
relation: "ex:ingredient",
|
|
315
|
+
select: [{ name: "label", property: "ex:label" }],
|
|
316
|
+
},
|
|
317
|
+
]),
|
|
318
|
+
"test",
|
|
319
|
+
);
|
|
320
|
+
expect(validated.lookup?.expand?.[0]?.name).toBe("ingredients");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("rejects an expand with an empty select", () => {
|
|
324
|
+
expect(() =>
|
|
325
|
+
validateStoryPackDefinition(
|
|
326
|
+
withExpand([
|
|
327
|
+
{ name: "ingredients", relation: "ex:ingredient", select: [] },
|
|
328
|
+
]),
|
|
329
|
+
"test",
|
|
330
|
+
),
|
|
331
|
+
).toThrow(/select/);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("rejects an expand kind other than list or table", () => {
|
|
335
|
+
expect(() =>
|
|
336
|
+
validateStoryPackDefinition(
|
|
337
|
+
withExpand([
|
|
338
|
+
{
|
|
339
|
+
name: "ingredients",
|
|
340
|
+
relation: "ex:ingredient",
|
|
341
|
+
kind: "tree",
|
|
342
|
+
select: [{ name: "label", property: "ex:label" }],
|
|
343
|
+
},
|
|
344
|
+
]),
|
|
345
|
+
"test",
|
|
346
|
+
),
|
|
347
|
+
).toThrow(/must be "list" or "table"/);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("rejects duplicate expand names", () => {
|
|
351
|
+
expect(() =>
|
|
352
|
+
validateStoryPackDefinition(
|
|
353
|
+
withExpand([
|
|
354
|
+
{
|
|
355
|
+
name: "dup",
|
|
356
|
+
relation: "ex:a",
|
|
357
|
+
select: [{ name: "x", property: "ex:x" }],
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "dup",
|
|
361
|
+
relation: "ex:b",
|
|
362
|
+
select: [{ name: "y", property: "ex:y" }],
|
|
363
|
+
},
|
|
364
|
+
]),
|
|
365
|
+
"test",
|
|
366
|
+
),
|
|
367
|
+
).toThrow(/duplicate expand name/);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/** RECIPE_STORY with the given `lookup` extras merged in. */
|
|
371
|
+
function withLookupExtras(extras: Record<string, unknown>): unknown {
|
|
372
|
+
return {
|
|
373
|
+
...RECIPE_STORY,
|
|
374
|
+
lookup: { ...RECIPE_STORY.lookup, ...extras },
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
it("accepts a valid disclosure with a level-gated expand", () => {
|
|
379
|
+
const validated = validateStoryPackDefinition(
|
|
380
|
+
withLookupExtras({
|
|
381
|
+
disclosure: { levels: ["summary", "detailed"], default: "summary" },
|
|
382
|
+
expand: [
|
|
383
|
+
{
|
|
384
|
+
name: "ingredients",
|
|
385
|
+
relation: "ex:ingredient",
|
|
386
|
+
select: [{ name: "label", property: "ex:label" }],
|
|
387
|
+
level: "detailed",
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
}),
|
|
391
|
+
"test",
|
|
392
|
+
);
|
|
393
|
+
expect(validated.lookup?.disclosure?.levels).toEqual([
|
|
394
|
+
"summary",
|
|
395
|
+
"detailed",
|
|
396
|
+
]);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("rejects empty disclosure levels", () => {
|
|
400
|
+
expect(() =>
|
|
401
|
+
validateStoryPackDefinition(
|
|
402
|
+
withLookupExtras({ disclosure: { levels: [] } }),
|
|
403
|
+
"test",
|
|
404
|
+
),
|
|
405
|
+
).toThrow(/non-empty array/);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("rejects a disclosure default not among the levels", () => {
|
|
409
|
+
expect(() =>
|
|
410
|
+
validateStoryPackDefinition(
|
|
411
|
+
withLookupExtras({
|
|
412
|
+
disclosure: { levels: ["summary"], default: "detailed" },
|
|
413
|
+
}),
|
|
414
|
+
"test",
|
|
415
|
+
),
|
|
416
|
+
).toThrow(/default "detailed" is not one of/);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("rejects a disclosure level named detail (derived param collision)", () => {
|
|
420
|
+
expect(() =>
|
|
421
|
+
validateStoryPackDefinition(
|
|
422
|
+
withLookupExtras({ disclosure: { levels: ["summary", "detail"] } }),
|
|
423
|
+
"test",
|
|
424
|
+
),
|
|
425
|
+
).toThrow(/reserved/);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("rejects a disclosure level colliding with a kernel param name", () => {
|
|
429
|
+
expect(() =>
|
|
430
|
+
validateStoryPackDefinition(
|
|
431
|
+
withLookupExtras({ disclosure: { levels: ["summary", "names"] } }),
|
|
432
|
+
"test",
|
|
433
|
+
),
|
|
434
|
+
).toThrow(/reserved/);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("rejects a disclosure level that is not a single lowercase word", () => {
|
|
438
|
+
expect(() =>
|
|
439
|
+
validateStoryPackDefinition(
|
|
440
|
+
withLookupExtras({ disclosure: { levels: ["summary", "Fully-Deep"] } }),
|
|
441
|
+
"test",
|
|
442
|
+
),
|
|
443
|
+
).toThrow(/single lowercase word/);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("accepts a disclosure level named detailed (the legacy alias)", () => {
|
|
447
|
+
const validated = validateStoryPackDefinition(
|
|
448
|
+
withLookupExtras({ disclosure: { levels: ["summary", "detailed"] } }),
|
|
449
|
+
"test",
|
|
450
|
+
);
|
|
451
|
+
expect(validated.lookup?.disclosure?.levels).toEqual([
|
|
452
|
+
"summary",
|
|
453
|
+
"detailed",
|
|
454
|
+
]);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("rejects an expand level that is not a declared disclosure level", () => {
|
|
458
|
+
expect(() =>
|
|
459
|
+
validateStoryPackDefinition(
|
|
460
|
+
withLookupExtras({
|
|
461
|
+
disclosure: { levels: ["summary"] },
|
|
462
|
+
expand: [
|
|
463
|
+
{
|
|
464
|
+
name: "ingredients",
|
|
465
|
+
relation: "ex:ingredient",
|
|
466
|
+
select: [{ name: "label", property: "ex:label" }],
|
|
467
|
+
level: "detailed",
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
}),
|
|
471
|
+
"test",
|
|
472
|
+
),
|
|
473
|
+
).toThrow(/is not a declared disclosure level/);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe("validateStoryPackDefinition — identifier hardening", () => {
|
|
478
|
+
const base = JSON.parse(JSON.stringify(RECIPE_STORY)) as Record<
|
|
479
|
+
string,
|
|
480
|
+
unknown
|
|
481
|
+
>;
|
|
482
|
+
|
|
483
|
+
it("rejects a column field the query never mentions", () => {
|
|
484
|
+
const list = {
|
|
485
|
+
...(base.list as Record<string, unknown>),
|
|
486
|
+
columns: [{ field: "name" }, { field: "season" }],
|
|
487
|
+
};
|
|
488
|
+
expect(() =>
|
|
489
|
+
validateStoryPackDefinition({ ...base, list }, "test"),
|
|
490
|
+
).toThrow(/column field "\?season" does not appear in "list.query"/);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("validates verb columns against the verb query", () => {
|
|
494
|
+
expect(() =>
|
|
495
|
+
validateStoryPackDefinition(
|
|
496
|
+
{
|
|
497
|
+
...RECIPE_STORY,
|
|
498
|
+
verbs: [
|
|
499
|
+
{
|
|
500
|
+
verb: "categories",
|
|
501
|
+
query: "SELECT ?name WHERE { ?uri ex:category ?name }",
|
|
502
|
+
columns: [{ field: "count" }],
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
"test",
|
|
507
|
+
),
|
|
508
|
+
).toThrow(/column field "\?count" does not appear in "verbs\[0\].query"/);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("accepts a column bound via AS in the SELECT projection", () => {
|
|
512
|
+
const validated = validateStoryPackDefinition(
|
|
513
|
+
{
|
|
514
|
+
...RECIPE_STORY,
|
|
515
|
+
verbs: [
|
|
516
|
+
{
|
|
517
|
+
verb: "categories",
|
|
518
|
+
query:
|
|
519
|
+
"SELECT ?name (COUNT(?uri) AS ?count) WHERE { ?uri ex:category ?name } GROUP BY ?name",
|
|
520
|
+
columns: [{ field: "name" }, { field: "count" }],
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
"test",
|
|
525
|
+
);
|
|
526
|
+
expect(validated.verbs?.at(0)?.columns).toHaveLength(2);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Reserved object-prototype keys: pack identifiers become plain-object
|
|
530
|
+
// keys at run time, so __proto__/constructor/prototype must fail at boot.
|
|
531
|
+
it("rejects a reserved object key as a column field", () => {
|
|
532
|
+
const list = {
|
|
533
|
+
...(base.list as Record<string, unknown>),
|
|
534
|
+
query: "SELECT ?__proto__ WHERE { ?uri ex:name ?__proto__ }",
|
|
535
|
+
columns: [{ field: "__proto__" }],
|
|
536
|
+
filters: undefined,
|
|
537
|
+
};
|
|
538
|
+
expect(() =>
|
|
539
|
+
validateStoryPackDefinition({ ...base, list }, "test"),
|
|
540
|
+
).toThrow(/reserved object key "__proto__"/);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("rejects a reserved object key as a filter param or variable", () => {
|
|
544
|
+
const withFilter = (filter: Record<string, unknown>): unknown => ({
|
|
545
|
+
...base,
|
|
546
|
+
list: {
|
|
547
|
+
...(base.list as Record<string, unknown>),
|
|
548
|
+
query:
|
|
549
|
+
"SELECT ?uri ?name ?category ?constructor WHERE { ?uri ex:name ?name ; ex:category ?category ; ex:kind ?constructor }",
|
|
550
|
+
filters: [filter],
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
expect(() =>
|
|
554
|
+
validateStoryPackDefinition(
|
|
555
|
+
withFilter({ param: "constructor", variable: "category" }),
|
|
556
|
+
"test",
|
|
557
|
+
),
|
|
558
|
+
).toThrow(/reserved object key "constructor"/);
|
|
559
|
+
expect(() =>
|
|
560
|
+
validateStoryPackDefinition(
|
|
561
|
+
withFilter({ param: "kind", variable: "constructor" }),
|
|
562
|
+
"test",
|
|
563
|
+
),
|
|
564
|
+
).toThrow(/reserved object key "constructor"/);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("rejects a reserved object key as a search variable", () => {
|
|
568
|
+
expect(() =>
|
|
569
|
+
validateStoryPackDefinition(
|
|
570
|
+
{
|
|
571
|
+
...base,
|
|
572
|
+
list: {
|
|
573
|
+
...(base.list as Record<string, unknown>),
|
|
574
|
+
query:
|
|
575
|
+
"SELECT ?uri ?name ?category ?prototype WHERE { ?uri ex:name ?name ; ex:category ?category ; ex:proto ?prototype }",
|
|
576
|
+
search: { variables: ["prototype"] },
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
"test",
|
|
580
|
+
),
|
|
581
|
+
).toThrow(/reserved object key "prototype"/);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("rejects a reserved object key as a lookup field or expand name", () => {
|
|
585
|
+
expect(() =>
|
|
586
|
+
validateStoryPackDefinition(
|
|
587
|
+
{
|
|
588
|
+
...RECIPE_STORY,
|
|
589
|
+
lookup: {
|
|
590
|
+
...RECIPE_STORY.lookup,
|
|
591
|
+
fields: [{ name: "__proto__", property: "ex:category" }],
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
"test",
|
|
595
|
+
),
|
|
596
|
+
).toThrow(/reserved object key "__proto__"/);
|
|
597
|
+
expect(() =>
|
|
598
|
+
validateStoryPackDefinition(
|
|
599
|
+
{
|
|
600
|
+
...RECIPE_STORY,
|
|
601
|
+
lookup: {
|
|
602
|
+
...RECIPE_STORY.lookup,
|
|
603
|
+
expand: [
|
|
604
|
+
{
|
|
605
|
+
name: "__proto__",
|
|
606
|
+
relation: "ex:ingredient",
|
|
607
|
+
select: [{ name: "label", property: "ex:label" }],
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
"test",
|
|
613
|
+
),
|
|
614
|
+
).toThrow(/reserved object key "__proto__"/);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("rejects a reserved object key inside an expand select", () => {
|
|
618
|
+
expect(() =>
|
|
619
|
+
validateStoryPackDefinition(
|
|
620
|
+
{
|
|
621
|
+
...RECIPE_STORY,
|
|
622
|
+
lookup: {
|
|
623
|
+
...RECIPE_STORY.lookup,
|
|
624
|
+
expand: [
|
|
625
|
+
{
|
|
626
|
+
name: "ingredients",
|
|
627
|
+
relation: "ex:ingredient",
|
|
628
|
+
select: [{ name: "constructor", property: "ex:label" }],
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
"test",
|
|
634
|
+
),
|
|
635
|
+
).toThrow(/reserved object key "constructor"/);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("rejects a reserved object key as a disclosure level", () => {
|
|
639
|
+
expect(() =>
|
|
640
|
+
validateStoryPackDefinition(
|
|
641
|
+
{
|
|
642
|
+
...RECIPE_STORY,
|
|
643
|
+
lookup: {
|
|
644
|
+
...RECIPE_STORY.lookup,
|
|
645
|
+
disclosure: { levels: ["summary", "prototype"] },
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
"test",
|
|
649
|
+
),
|
|
650
|
+
).toThrow(/reserved object key "prototype"/);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Pack v1: extra list verbs and the sample capability.
|
|
655
|
+
describe("validateStoryPackDefinition — verbs and sample", () => {
|
|
656
|
+
const VERB = {
|
|
657
|
+
verb: "categories",
|
|
658
|
+
query: "SELECT ?name WHERE { ?uri ex:category ?name }",
|
|
659
|
+
columns: [{ field: "name" }],
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
function withVerbs(verbs: unknown): unknown {
|
|
663
|
+
return { ...RECIPE_STORY, verbs };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
it("accepts a valid extra verb and validates its list shape", () => {
|
|
667
|
+
const validated = validateStoryPackDefinition(withVerbs([VERB]), "test");
|
|
668
|
+
expect(validated.verbs?.at(0)?.verb).toBe("categories");
|
|
669
|
+
expect(validated.verbs?.at(0)?.columns).toEqual([{ field: "name" }]);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("rejects a verb colliding with a compiled verb", () => {
|
|
673
|
+
for (const verb of ["list", "lookup", "sample"]) {
|
|
674
|
+
expect(() =>
|
|
675
|
+
validateStoryPackDefinition(withVerbs([{ ...VERB, verb }]), "test"),
|
|
676
|
+
).toThrow(/collides with a compiled verb/);
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("rejects duplicate verbs", () => {
|
|
681
|
+
expect(() =>
|
|
682
|
+
validateStoryPackDefinition(withVerbs([VERB, VERB]), "test"),
|
|
683
|
+
).toThrow(/duplicate verb/);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("rejects a non-kebab verb", () => {
|
|
687
|
+
expect(() =>
|
|
688
|
+
validateStoryPackDefinition(
|
|
689
|
+
withVerbs([{ ...VERB, verb: "Cats" }]),
|
|
690
|
+
"test",
|
|
691
|
+
),
|
|
692
|
+
).toThrow(/kebab-case/);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("rejects a verb whose query is not a SELECT", () => {
|
|
696
|
+
expect(() =>
|
|
697
|
+
validateStoryPackDefinition(
|
|
698
|
+
withVerbs([{ ...VERB, query: "ASK { ?s ?p ?o }" }]),
|
|
699
|
+
"test",
|
|
700
|
+
),
|
|
701
|
+
).toThrow(/verbs\[0\].query/);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("validates verb filters against the verb query", () => {
|
|
705
|
+
expect(() =>
|
|
706
|
+
validateStoryPackDefinition(
|
|
707
|
+
withVerbs([
|
|
708
|
+
{
|
|
709
|
+
...VERB,
|
|
710
|
+
filters: [{ param: "season", variable: "season", values: ["x"] }],
|
|
711
|
+
},
|
|
712
|
+
]),
|
|
713
|
+
"test",
|
|
714
|
+
),
|
|
715
|
+
).toThrow(/does not appear in "verbs\[0\].query"/);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
function withSample(sample: unknown): unknown {
|
|
719
|
+
return { ...RECIPE_STORY, lookup: { ...RECIPE_STORY.lookup, sample } };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
it("accepts sample: true", () => {
|
|
723
|
+
const validated = validateStoryPackDefinition(withSample(true), "test");
|
|
724
|
+
expect(validated.lookup?.sample).toBe(true);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("accepts a configured sample with a valid default count", () => {
|
|
728
|
+
const validated = validateStoryPackDefinition(
|
|
729
|
+
withSample({ count: 3 }),
|
|
730
|
+
"test",
|
|
731
|
+
);
|
|
732
|
+
expect(validated.lookup?.sample).toEqual({ count: 3 });
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("rejects an out-of-range or non-integer sample count", () => {
|
|
736
|
+
for (const count of [0, 6, 2.5, "2"]) {
|
|
737
|
+
expect(() =>
|
|
738
|
+
validateStoryPackDefinition(withSample({ count }), "test"),
|
|
739
|
+
).toThrow(/sample.count/);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
240
742
|
});
|