@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,37 +1,204 @@
1
1
  /**
2
- * Prefix map for ke store creation.
2
+ * Prefix maps for ke store creation and display compaction.
3
3
  *
4
- * Registered as PREFIX declarations in every SPARQL query.
5
- * Single source of truth — rename a key here and P updates automatically.
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
- ds: "https://ds.canonical.com/",
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
- * SPARQL prefix accessors derived from {@link PREFIX_MAP}.
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 PREFIX_MAP automatically updates every query.
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(PREFIX_MAP).map((k) => [k, `${k}:`]),
26
- ) as { readonly [K in keyof typeof PREFIX_MAP]: `${K}:` };
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 PREFIX_MAP}.
30
- *
31
- * Includes `xsd:` which TTL data files commonly use but SPARQL queries
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(PREFIX_MAP).map(([k, uri]) => `@prefix ${k}: <${uri}> .`),
36
- "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .",
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 { PREFIX_MAP } from "./prefixes.js";
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 ?? PREFIX_MAP;
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 ?? PREFIX_MAP;
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 ?? PREFIX_MAP;
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 ?? PREFIX_MAP;
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
+ });