@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.
Files changed (254) hide show
  1. package/README.md +64 -35
  2. package/dist/pragma +0 -0
  3. package/package.json +15 -13
  4. package/src/completions/constants.ts +7 -0
  5. package/src/completions/handleQuery.test.ts +32 -0
  6. package/src/completions/handleQuery.ts +20 -1
  7. package/src/completions/queryCompletions.test.ts +5 -0
  8. package/src/completions/queryCompletions.ts +17 -3
  9. package/src/completions/querySocket.test.ts +31 -0
  10. package/src/completions/querySocket.ts +36 -9
  11. package/src/config/parseConfigValues.ts +2 -0
  12. package/src/config/readConfigLayers.test.ts +2 -0
  13. package/src/config/readConfigLayers.ts +3 -0
  14. package/src/config/types.ts +7 -0
  15. package/src/constants.ts +3 -1
  16. package/src/domains/block/commands/sample.ts +2 -1
  17. package/src/domains/block/formatters/list.test.ts +3 -3
  18. package/src/domains/block/formatters/lookup.test.ts +5 -5
  19. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  20. package/src/domains/block/stories.ts +9 -3
  21. package/src/domains/config/commands/channel.test.ts +16 -0
  22. package/src/domains/config/commands/channel.ts +14 -1
  23. package/src/domains/config/commands/framework.test.ts +14 -0
  24. package/src/domains/config/commands/framework.ts +13 -1
  25. package/src/domains/config/commands/tier.test.ts +7 -0
  26. package/src/domains/config/commands/tier.ts +14 -1
  27. package/src/domains/config/operations/validateTier.test.ts +9 -0
  28. package/src/domains/config/operations/validateTier.ts +10 -2
  29. package/src/domains/create/generatorPack/bundled.ts +49 -0
  30. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  31. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  32. package/src/domains/create/index.ts +13 -7
  33. package/src/domains/create/mcp/specs.ts +6 -161
  34. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  35. package/src/domains/create/renderGeneratorUi.ts +65 -0
  36. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  37. package/src/domains/doctor/formatters/doctor.ts +90 -38
  38. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
  39. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
  40. package/src/domains/doctor/operations/checks.test.ts +6 -4
  41. package/src/domains/doctor/operations/types.ts +23 -1
  42. package/src/domains/graph/commands/inspect.test.ts +2 -2
  43. package/src/domains/graph/commands/query.test.ts +5 -3
  44. package/src/domains/graph/formatters/inspect.ts +7 -7
  45. package/src/domains/graph/formatters/query.test.ts +87 -0
  46. package/src/domains/graph/formatters/query.ts +42 -5
  47. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  48. package/src/domains/graphql/commands/serve.test.ts +12 -6
  49. package/src/domains/graphql/commands/serve.ts +16 -6
  50. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  51. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  52. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  53. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  54. package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
  55. package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
  56. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  57. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  58. package/src/domains/graphql/operations/index.ts +5 -0
  59. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  60. package/src/domains/info/formatters/upgrade.ts +3 -1
  61. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  62. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  63. package/src/domains/info/operations/index.ts +1 -0
  64. package/src/domains/info/types.ts +13 -0
  65. package/src/domains/llm/capabilities.test.ts +23 -3
  66. package/src/domains/llm/commands/capabilities.ts +12 -3
  67. package/src/domains/llm/commands/llm.ts +5 -1
  68. package/src/domains/llm/data/commandReference.ts +20 -11
  69. package/src/domains/llm/data/decisionTrees.ts +10 -5
  70. package/src/domains/llm/data/toolCatalog.ts +49 -18
  71. package/src/domains/llm/formatters/orientation.ts +5 -1
  72. package/src/domains/llm/llmCommand.test.ts +11 -3
  73. package/src/domains/llm/mcp/specs.ts +3 -4
  74. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  75. package/src/domains/llm/operations/collectContext.ts +7 -17
  76. package/src/domains/modifier/commands/sample.ts +2 -1
  77. package/src/domains/modifier/operations/lookup.ts +5 -3
  78. package/src/domains/modifier/stories.ts +9 -0
  79. package/src/domains/ontology/commands/list.test.ts +5 -3
  80. package/src/domains/ontology/commands/show.test.ts +85 -9
  81. package/src/domains/ontology/formatters/index.ts +1 -0
  82. package/src/domains/ontology/formatters/list.ts +37 -12
  83. package/src/domains/ontology/formatters/show.ts +358 -67
  84. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  85. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  86. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  87. package/src/domains/ontology/helpers/index.ts +14 -0
  88. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  89. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  90. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  91. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  92. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  93. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  94. package/src/domains/ontology/operations/showOntology.ts +289 -7
  95. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  96. package/src/domains/ontology/stories.ts +53 -8
  97. package/src/domains/refs/operations/updateRefs.ts +0 -1
  98. package/src/domains/setup/commands/index.ts +1 -0
  99. package/src/domains/setup/commands/setup.ts +70 -0
  100. package/src/domains/setup/index.ts +7 -3
  101. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  102. package/src/domains/setup/operations/setupLsp.ts +7 -2
  103. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  104. package/src/domains/shared/bootStore.test.ts +82 -0
  105. package/src/domains/shared/bootStore.ts +56 -20
  106. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  107. package/src/domains/shared/extractLocalName.test.ts +3 -3
  108. package/src/domains/shared/formatters.ts +23 -2
  109. package/src/domains/shared/listTiers.ts +40 -0
  110. package/src/domains/shared/loaders/bundledLoader.ts +48 -0
  111. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  112. package/src/domains/shared/loaders/localLoader.ts +2 -18
  113. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  114. package/src/domains/shared/loaders/readPackageDir.ts +46 -8
  115. package/src/domains/shared/lookupMany.test.ts +15 -0
  116. package/src/domains/shared/lookupMany.ts +22 -6
  117. package/src/domains/shared/namespaces.ts +13 -5
  118. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  119. package/src/domains/shared/parseSampleCount.ts +45 -0
  120. package/src/domains/shared/prefixes.test.ts +211 -0
  121. package/src/domains/shared/prefixes.ts +182 -15
  122. package/src/domains/shared/renderers.ts +5 -5
  123. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  124. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  125. package/src/domains/shared/semanticPackage.test.ts +89 -0
  126. package/src/domains/shared/semanticPackage.ts +17 -2
  127. package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
  128. package/src/domains/shared/stories/compileLookupTool.ts +1 -1
  129. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
  130. package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
  131. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  132. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  133. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
  134. package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
  135. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  136. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  137. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  138. package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
  139. package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
  140. package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
  141. package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
  142. package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
  143. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
  144. package/src/domains/shared/stories/pack/index.ts +14 -0
  145. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  146. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  147. package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
  148. package/src/domains/shared/stories/pack/types.ts +123 -3
  149. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
  150. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
  151. package/src/domains/shared/stories/types.ts +9 -1
  152. package/src/domains/shared/types/index.ts +5 -9
  153. package/src/domains/shared/types/ontology.ts +119 -24
  154. package/src/domains/shared/types/standard.ts +9 -52
  155. package/src/domains/token/commands/lookup.test.ts +8 -0
  156. package/src/domains/token/commands/lookup.ts +12 -0
  157. package/src/domains/token/commands/sample.ts +2 -1
  158. package/src/domains/token/featureFlag.ts +17 -0
  159. package/src/domains/token/index.ts +5 -0
  160. package/src/domains/token/mcp/specs.ts +13 -1
  161. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  162. package/src/domains/trace/commands/show.ts +30 -4
  163. package/src/index.ts +1 -13
  164. package/src/mcp/registerResources.test.ts +2 -2
  165. package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
  166. package/src/mcp/tools/index.ts +18 -10
  167. package/src/mcp/tools/registerTools.test.ts +106 -30
  168. package/src/pipeline/collectCommands.test.ts +151 -1
  169. package/src/pipeline/collectCommands.ts +34 -19
  170. package/src/pipeline/createProgram.test.ts +5 -1
  171. package/src/pipeline/createProgram.ts +2 -2
  172. package/src/pipeline/firstRun.test.ts +80 -0
  173. package/src/pipeline/firstRun.ts +99 -0
  174. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  175. package/src/pipeline/parseGlobalFlags.ts +98 -9
  176. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  177. package/src/pipeline/resolveCommandKind.ts +4 -0
  178. package/src/pipeline/rootHelp.test.ts +87 -0
  179. package/src/pipeline/rootHelp.ts +265 -0
  180. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  181. package/src/pipeline/runCli.test.ts +17 -4
  182. package/src/pipeline/runCli.ts +162 -19
  183. package/src/testing/cli.ts +30 -2
  184. package/src/testing/dsFixtures.ts +21 -4
  185. package/src/testing/index.ts +1 -4
  186. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
  187. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  188. package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
  189. package/src/testing/integration/edge-cases.test.ts +62 -50
  190. package/src/testing/integration/executor-execution.test.ts +6 -4
  191. package/src/testing/integration/executor-golden.test.ts +2 -2
  192. package/src/testing/integration/mcp-surface.test.ts +40 -15
  193. package/src/testing/integration/parity.test.ts +119 -70
  194. package/src/testing/setupXdgIsolation.ts +16 -5
  195. package/src/testing/standardParityFixtures.ts +26 -83
  196. package/src/testing/store.ts +4 -4
  197. package/src/domains/create/commands/component.test.ts +0 -166
  198. package/src/domains/create/commands/component.ts +0 -123
  199. package/src/domains/create/commands/index.ts +0 -3
  200. package/src/domains/create/commands/package.test.ts +0 -35
  201. package/src/domains/create/commands/package.ts +0 -21
  202. package/src/domains/create/generators.ts +0 -8
  203. package/src/domains/standard/commands/categories.test.ts +0 -70
  204. package/src/domains/standard/commands/categories.ts +0 -48
  205. package/src/domains/standard/commands/index.ts +0 -6
  206. package/src/domains/standard/commands/list.test.ts +0 -105
  207. package/src/domains/standard/commands/list.ts +0 -11
  208. package/src/domains/standard/commands/lookup.test.ts +0 -135
  209. package/src/domains/standard/commands/lookup.ts +0 -14
  210. package/src/domains/standard/commands/sample.ts +0 -57
  211. package/src/domains/standard/formatters/categories.test.ts +0 -44
  212. package/src/domains/standard/formatters/categories.ts +0 -38
  213. package/src/domains/standard/formatters/index.ts +0 -10
  214. package/src/domains/standard/formatters/list.test.ts +0 -169
  215. package/src/domains/standard/formatters/list.ts +0 -184
  216. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  217. package/src/domains/standard/formatters/lookup.ts +0 -101
  218. package/src/domains/standard/formatters/sample.ts +0 -15
  219. package/src/domains/standard/formatters/types.ts +0 -31
  220. package/src/domains/standard/index.ts +0 -40
  221. package/src/domains/standard/mcp/index.ts +0 -2
  222. package/src/domains/standard/mcp/specs.ts +0 -89
  223. package/src/domains/standard/operations/categories.test.ts +0 -61
  224. package/src/domains/standard/operations/categories.ts +0 -38
  225. package/src/domains/standard/operations/index.ts +0 -6
  226. package/src/domains/standard/operations/list.test.ts +0 -75
  227. package/src/domains/standard/operations/list.ts +0 -64
  228. package/src/domains/standard/operations/lookup.test.ts +0 -58
  229. package/src/domains/standard/operations/lookup.ts +0 -151
  230. package/src/domains/standard/operations/sample.ts +0 -35
  231. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  232. package/src/domains/standard/orchestration/index.ts +0 -4
  233. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  234. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
  235. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  236. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  237. package/src/domains/standard/standardConfig.ts +0 -42
  238. package/src/domains/standard/stories.test.ts +0 -24
  239. package/src/domains/standard/stories.ts +0 -151
  240. package/src/domains/standard/types.ts +0 -15
  241. package/src/domains/tier/commands/index.ts +0 -3
  242. package/src/domains/tier/commands/list.test.ts +0 -67
  243. package/src/domains/tier/commands/list.ts +0 -9
  244. package/src/domains/tier/formatters/index.ts +0 -3
  245. package/src/domains/tier/formatters/list.test.ts +0 -33
  246. package/src/domains/tier/formatters/list.ts +0 -45
  247. package/src/domains/tier/index.ts +0 -17
  248. package/src/domains/tier/mcp/index.ts +0 -2
  249. package/src/domains/tier/mcp/specs.ts +0 -12
  250. package/src/domains/tier/operations/index.ts +0 -3
  251. package/src/domains/tier/operations/list.test.ts +0 -32
  252. package/src/domains/tier/operations/list.ts +0 -33
  253. package/src/domains/tier/stories.test.ts +0 -22
  254. package/src/domains/tier/stories.ts +0 -52
@@ -1,21 +1,74 @@
1
1
  import { PragmaError } from "#error";
2
+ import { MAX_SAMPLE_COUNT, MIN_SAMPLE_COUNT } from "../../parseSampleCount.js";
2
3
  import type {
3
4
  StoryPackColumn,
4
5
  StoryPackDefinition,
6
+ StoryPackDisclosure,
7
+ StoryPackExpand,
8
+ StoryPackExpandField,
5
9
  StoryPackField,
6
10
  StoryPackFilter,
11
+ StoryPackList,
7
12
  StoryPackLookup,
13
+ StoryPackSample,
14
+ StoryPackSearch,
8
15
  StoryPackSection,
16
+ StoryPackVerb,
9
17
  } from "./types.js";
10
18
 
11
19
  const NOUN_PATTERN = /^[a-z][a-z0-9-]*$/;
12
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
+ }
13
54
  /** A prefixed name (`ex:Recipe`) — prefix + local part, no path slashes. */
14
55
  const PREFIXED_NAME_PATTERN = /^[A-Za-z][\w-]*:[^/<>"\s]+$/;
15
56
 
16
57
  /** An absolute IRI that can be safely embedded as `<iri>` in SPARQL. */
17
58
  const EMBEDDABLE_IRI_PATTERN = /^[A-Za-z][\w+.-]*:\/\/[^<>"\s]+$/;
18
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
+
19
72
  /**
20
73
  * A filter parameter name: a single lowercase word.
21
74
  *
@@ -30,16 +83,40 @@ const FILTER_PARAM_PATTERN = /^[a-z][a-z0-9]*$/;
30
83
 
31
84
  /**
32
85
  * Parameter names taken by global flags or parameters the kernel appends
33
- * to every read story — `condensed` is added to every MCP list tool, and
34
- * `detailed` to every lookup — so a filter may not reuse them.
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.
35
90
  */
36
91
  const RESERVED_FILTER_PARAMS = new Set([
37
92
  "llm",
38
93
  "format",
39
94
  "verbose",
95
+ "detail",
40
96
  "detailed",
41
97
  "condensed",
42
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",
43
120
  "help",
44
121
  ]);
45
122
 
@@ -78,6 +155,7 @@ export default function validateStoryPackDefinition(
78
155
  : requireString(obj.toolDescription, "toolDescription", source);
79
156
 
80
157
  const list = validateList(obj.list, source);
158
+ const verbs = validateVerbs(obj.verbs, source);
81
159
  const lookup =
82
160
  obj.lookup === undefined ? undefined : validateLookup(obj.lookup, source);
83
161
 
@@ -86,16 +164,87 @@ export default function validateStoryPackDefinition(
86
164
  ...(description !== undefined ? { description } : {}),
87
165
  ...(toolDescription !== undefined ? { toolDescription } : {}),
88
166
  list,
167
+ ...(verbs ? { verbs } : {}),
89
168
  ...(lookup ? { lookup } : {}),
90
169
  };
91
170
  }
92
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
+
93
228
  function validateList(
94
229
  raw: unknown,
95
230
  source: string,
96
231
  ): StoryPackDefinition["list"] {
97
- const obj = requireObject(raw, "list", source);
98
- const query = requireString(obj.query, "list.query", source);
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);
99
248
  // Strip leading PREFIX declarations so the real query verb is checked —
100
249
  // "PREFIX ex: <…> CONSTRUCT {…}" must fail here, not at first use.
101
250
  const afterPrefixes = query.replace(
@@ -105,31 +254,104 @@ function validateList(
105
254
  if (!/^\s*SELECT\s/i.test(afterPrefixes)) {
106
255
  throw buildStoryConfigError(
107
256
  source,
108
- '"list.query" must be a SPARQL SELECT query.',
257
+ `"${where}.query" must be a SPARQL SELECT query.`,
109
258
  );
110
259
  }
111
260
 
112
261
  if (!Array.isArray(obj.columns) || obj.columns.length === 0) {
113
262
  throw buildStoryConfigError(
114
263
  source,
115
- '"list.columns" must be a non-empty array.',
264
+ `"${where}.columns" must be a non-empty array.`,
116
265
  );
117
266
  }
118
267
  const columns = obj.columns.map((column, index) =>
119
- validateColumn(column, `list.columns[${index}]`, source),
268
+ validateColumn(column, `${where}.columns[${index}]`, source),
120
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
+ }
121
281
 
122
- const filters = validateFilterArray(obj.filters, source);
282
+ const filters = validateFilterArray(obj.filters, where, source);
123
283
  for (const filter of filters ?? []) {
124
284
  if (!referencesVariable(query, filter.variable)) {
125
285
  throw buildStoryConfigError(
126
286
  source,
127
- `filter variable "?${filter.variable}" does not appear in "list.query".`,
287
+ `filter variable "?${filter.variable}" does not appear in "${where}.query".`,
128
288
  );
129
289
  }
130
290
  }
131
291
 
132
- return { query, columns, ...(filters ? { filters } : {}) };
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
+ };
133
355
  }
134
356
 
135
357
  /**
@@ -145,17 +367,18 @@ function referencesVariable(query: string, variable: string): boolean {
145
367
 
146
368
  function validateFilterArray(
147
369
  raw: unknown,
370
+ where: string,
148
371
  source: string,
149
372
  ): readonly StoryPackFilter[] | undefined {
150
373
  if (raw === undefined) return undefined;
151
374
  if (!Array.isArray(raw) || raw.length === 0) {
152
375
  throw buildStoryConfigError(
153
376
  source,
154
- '"list.filters" must be a non-empty array.',
377
+ `"${where}.filters" must be a non-empty array.`,
155
378
  );
156
379
  }
157
380
  const filters = raw.map((entry, index) =>
158
- validateFilter(entry, `list.filters[${index}]`, source),
381
+ validateFilter(entry, `${where}.filters[${index}]`, source),
159
382
  );
160
383
  const seen = new Set<string>();
161
384
  for (const filter of filters) {
@@ -189,6 +412,7 @@ function validateFilter(
189
412
  `"${where}.param" "${param}" is a reserved name.`,
190
413
  );
191
414
  }
415
+ rejectReservedObjectKey(param, `${where}.param`, source);
192
416
  const variable = requireString(obj.variable, `${where}.variable`, source);
193
417
  if (!FIELD_PATTERN.test(variable)) {
194
418
  throw buildStoryConfigError(
@@ -196,13 +420,38 @@ function validateFilter(
196
420
  `"${where}.variable" must name a SELECT variable.`,
197
421
  );
198
422
  }
199
- if (!Array.isArray(obj.values) || obj.values.length === 0) {
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) {
200
449
  throw buildStoryConfigError(
201
450
  source,
202
451
  `"${where}.values" must be a non-empty array.`,
203
452
  );
204
453
  }
205
- const values = obj.values.map((value, index) =>
454
+ const values = raw.map((value, index) =>
206
455
  requireString(value, `${where}.values[${index}]`, source),
207
456
  );
208
457
  const lowered = new Set<string>();
@@ -217,16 +466,7 @@ function validateFilter(
217
466
  }
218
467
  lowered.add(key);
219
468
  }
220
- const description =
221
- obj.description === undefined
222
- ? undefined
223
- : requireString(obj.description, `${where}.description`, source);
224
- return {
225
- param,
226
- variable,
227
- values,
228
- ...(description !== undefined ? { description } : {}),
229
- };
469
+ return values;
230
470
  }
231
471
 
232
472
  function validateColumn(
@@ -242,6 +482,7 @@ function validateColumn(
242
482
  `"${where}.field" must name a SELECT variable.`,
243
483
  );
244
484
  }
485
+ rejectReservedObjectKey(field, `${where}.field`, source);
245
486
  const label =
246
487
  obj.label === undefined
247
488
  ? undefined
@@ -256,6 +497,14 @@ function validateLookup(raw: unknown, source: string): StoryPackLookup {
256
497
  obj.type === undefined
257
498
  ? undefined
258
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);
259
508
 
260
509
  const fields = validateFieldArray(obj.fields, "lookup.fields", source);
261
510
  const sections = validateSectionArray(
@@ -263,12 +512,213 @@ function validateLookup(raw: unknown, source: string): StoryPackLookup {
263
512
  "lookup.sections",
264
513
  source,
265
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
+ }
266
529
 
267
530
  return {
268
531
  by,
269
532
  ...(type !== undefined ? { type } : {}),
533
+ ...(description !== undefined ? { description } : {}),
534
+ ...(toolDescription !== undefined ? { toolDescription } : {}),
270
535
  ...(fields ? { fields } : {}),
271
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 } : {}),
272
722
  };
273
723
  }
274
724
 
@@ -324,6 +774,7 @@ function validateField(
324
774
  `"${where}.name" must be a simple identifier.`,
325
775
  );
326
776
  }
777
+ rejectReservedObjectKey(name, `${where}.name`, source);
327
778
  const property = requirePredicateTerm(
328
779
  obj.property,
329
780
  `${where}.property`,
@@ -141,11 +141,19 @@ export interface LookupStory<TDetailed, TFmtInput> {
141
141
  /** Named groups for organizing CLI parameters in help output. */
142
142
  readonly parameterGroups?: Readonly<Record<string, readonly string[]>>;
143
143
  readonly examples: readonly string[];
144
- /** Resolve all queries, collecting per-query failures. */
144
+ /**
145
+ * Resolve all queries, collecting per-query failures.
146
+ *
147
+ * The compilers pass the per-invocation `view` so surface-sensitive
148
+ * resolution (e.g. pack disclosure defaults, which differ between the
149
+ * CLI and MCP per the ratified surface contract) can see which surface
150
+ * is calling; most stories ignore it.
151
+ */
145
152
  readonly resolve: (
146
153
  rt: PragmaRuntime,
147
154
  names: readonly string[],
148
155
  params: Record<string, unknown>,
156
+ view?: LookupStoryView,
149
157
  ) => Promise<LookupResult<TDetailed>>;
150
158
  /**
151
159
  * Effective `detailed` for a surface. Defaults to the plain flag value
@@ -26,21 +26,17 @@ export type {
26
26
  } from "./modifier.js";
27
27
  export type {
28
28
  OntologyClass,
29
+ OntologyClassFocus,
30
+ OntologyConstraint,
29
31
  OntologyDetailed,
32
+ OntologyMeta,
30
33
  OntologyProperty,
34
+ OntologyQueryHint,
31
35
  OntologySummary,
32
36
  } from "./ontology.js";
33
37
  export type { PragmaRuntime } from "./runtime.js";
34
38
  export type { SampleOutput, SampleResult } from "./sample.js";
35
- export type {
36
- CategorySummary,
37
- CodeBlock,
38
- StandardDetailed,
39
- StandardDigest,
40
- StandardListFilters,
41
- StandardRef,
42
- StandardSummary,
43
- } from "./standard.js";
39
+ export type { StandardRef } from "./standard.js";
44
40
  export type { TierEntry } from "./tier.js";
45
41
  export type {
46
42
  TokenDetailed,