@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
@@ -9,12 +9,16 @@ import type { StoryPackFilter } from "./types.js";
9
9
  * query's ordering is preserved.
10
10
  *
11
11
  * Matching semantics:
12
- * - Input is trimmed, unicode-normalized (NFC), and matched against the
13
- * declared values case-insensitively; the DECLARED value is canonical
14
- * and is what rows are compared against.
15
- * - Row comparison is exact on the canonical value (NFC on both sides).
16
- * ke bindings carry lexical forms — language tags and datatypes are
17
- * already stripped — so plain equality is sound.
12
+ * - With declared `values`: input is trimmed, unicode-normalized (NFC),
13
+ * and matched against the declared values case-insensitively; the
14
+ * DECLARED value is canonical and rows are compared against it exactly
15
+ * (NFC on both sides).
16
+ * - Without `values` (value-free filter): the input itself is the term —
17
+ * trimmed, NFC-normalized — and rows match on case-insensitive equality
18
+ * with the variable's value. Nothing is rejected up front because the
19
+ * valid set lives in the data.
20
+ * - ke bindings carry lexical forms — language tags and datatypes are
21
+ * already stripped — so plain string comparison is sound.
18
22
  * - A row without a binding for the variable (e.g. from OPTIONAL) never
19
23
  * matches an active filter.
20
24
  * - Several provided filters combine conjunctively.
@@ -25,7 +29,8 @@ import type { StoryPackFilter } from "./types.js";
25
29
  * @returns Rows matching every provided filter.
26
30
  * @throws PragmaError with code `INVALID_INPUT` (carrying the declared
27
31
  * values as valid options) when a provided value is not in the
28
- * filter's declared set.
32
+ * filter's declared set, or when a value-free filter receives a
33
+ * non-string value.
29
34
  */
30
35
  export default function applyPackFilters(
31
36
  rows: Record<string, string>[],
@@ -39,7 +44,16 @@ export default function applyPackFilters(
39
44
  for (const filter of filters ?? []) {
40
45
  const provided = params[filter.param];
41
46
  if (provided === undefined) continue;
42
- const canonical = canonicalizeFilterValue(provided, filter);
47
+ const values = filter.values;
48
+ if (values === undefined) {
49
+ // Value-free filter: case-insensitive equality against the variable.
50
+ const term = requireStringValue(provided, filter).toLowerCase();
51
+ result = result.filter(
52
+ (row) => row[filter.variable]?.normalize("NFC").toLowerCase() === term,
53
+ );
54
+ continue;
55
+ }
56
+ const canonical = canonicalizeFilterValue(provided, filter, values);
43
57
  result = result.filter(
44
58
  (row) => row[filter.variable]?.normalize("NFC") === canonical,
45
59
  );
@@ -47,6 +61,26 @@ export default function applyPackFilters(
47
61
  return result;
48
62
  }
49
63
 
64
+ /**
65
+ * Require a value-free filter's input to be a trimmed, NFC-normalized
66
+ * string.
67
+ *
68
+ * @throws PragmaError with code `INVALID_INPUT` when it is not a string.
69
+ */
70
+ function requireStringValue(
71
+ provided: unknown,
72
+ filter: StoryPackFilter,
73
+ ): string {
74
+ if (typeof provided !== "string") {
75
+ throw PragmaError.invalidInput(filter.param, String(provided), {
76
+ recovery: {
77
+ message: `Provide a string value for --${filter.param}.`,
78
+ },
79
+ });
80
+ }
81
+ return provided.trim().normalize("NFC");
82
+ }
83
+
50
84
  /**
51
85
  * Resolve a provided filter value to its declared canonical form.
52
86
  *
@@ -56,10 +90,11 @@ export default function applyPackFilters(
56
90
  function canonicalizeFilterValue(
57
91
  provided: unknown,
58
92
  filter: StoryPackFilter,
93
+ values: readonly string[],
59
94
  ): string {
60
95
  if (typeof provided === "string") {
61
96
  const normalized = provided.trim().normalize("NFC");
62
- const match = filter.values.find(
97
+ const match = values.find(
63
98
  (value) =>
64
99
  value.normalize("NFC").toLowerCase() === normalized.toLowerCase(),
65
100
  );
@@ -68,9 +103,9 @@ function canonicalizeFilterValue(
68
103
  }
69
104
  }
70
105
  throw PragmaError.invalidInput(filter.param, String(provided), {
71
- validOptions: [...filter.values],
106
+ validOptions: [...values],
72
107
  recovery: {
73
- message: `Allowed values for --${filter.param}: ${filter.values.join(", ")}.`,
108
+ message: `Allowed values for --${filter.param}: ${values.join(", ")}.`,
74
109
  },
75
110
  });
76
111
  }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import applyPackSearch from "./applyPackSearch.js";
3
+ import type { StoryPackSearch } from "./types.js";
4
+
5
+ const ROWS = [
6
+ { name: "Gazpacho", description: "A cold soup" },
7
+ { name: "Pancakes", description: "Fried batter" },
8
+ { name: "Mystery" },
9
+ ];
10
+ const SEARCH: StoryPackSearch = { variables: ["name", "description"] };
11
+
12
+ describe("applyPackSearch", () => {
13
+ it("returns rows unchanged when no search is declared", () => {
14
+ expect(applyPackSearch(ROWS, undefined, { search: "soup" })).toEqual(ROWS);
15
+ });
16
+
17
+ it("returns rows unchanged when no term is provided", () => {
18
+ expect(applyPackSearch(ROWS, SEARCH, {})).toEqual(ROWS);
19
+ });
20
+
21
+ it("returns rows unchanged for an empty or whitespace term", () => {
22
+ expect(applyPackSearch(ROWS, SEARCH, { search: " " })).toEqual(ROWS);
23
+ });
24
+
25
+ it("keeps rows where any searched variable contains the term", () => {
26
+ const rows = applyPackSearch(ROWS, SEARCH, { search: "soup" });
27
+ expect(rows.map((row) => row.name)).toEqual(["Gazpacho"]);
28
+ });
29
+
30
+ it("matches case-insensitively", () => {
31
+ const rows = applyPackSearch(ROWS, SEARCH, { search: "PANCAKE" });
32
+ expect(rows.map((row) => row.name)).toEqual(["Pancakes"]);
33
+ });
34
+
35
+ it("matches substrings mid-value", () => {
36
+ const rows = applyPackSearch(ROWS, SEARCH, { search: "batter" });
37
+ expect(rows.map((row) => row.name)).toEqual(["Pancakes"]);
38
+ });
39
+
40
+ it("never matches rows without a binding for a searched variable", () => {
41
+ const rows = applyPackSearch(ROWS, SEARCH, { search: "myst" });
42
+ expect(rows.map((row) => row.name)).toEqual(["Mystery"]);
43
+ expect(applyPackSearch(ROWS, SEARCH, { search: "absent" })).toEqual([]);
44
+ });
45
+
46
+ it("matches unicode terms across NFC/NFD normalization forms", () => {
47
+ const rows = [{ name: "Entr\u00e9e" }];
48
+ const search: StoryPackSearch = { variables: ["name"] };
49
+ expect(
50
+ applyPackSearch(rows, search, { search: "entre\u0301e" }),
51
+ ).toHaveLength(1);
52
+ });
53
+
54
+ it("ignores non-string search values", () => {
55
+ expect(applyPackSearch(ROWS, SEARCH, { search: 3 })).toEqual(ROWS);
56
+ });
57
+ });
@@ -0,0 +1,41 @@
1
+ import type { StoryPackSearch } from "./types.js";
2
+
3
+ /**
4
+ * Apply a pack list's free-text search to resolved rows.
5
+ *
6
+ * Search is a row predicate on projected SELECT variables — like filters,
7
+ * it runs AFTER the author query, so user input never touches the query
8
+ * text (injection-safe by construction) and the author's ORDER BY is
9
+ * preserved.
10
+ *
11
+ * Matching semantics:
12
+ * - The term is NFC-normalized and matched case-insensitively.
13
+ * - A row passes when ANY declared variable's value contains the term as
14
+ * a substring; unbound variables (e.g. from OPTIONAL) never match.
15
+ * - A non-string or empty term is a no-op — search is a refinement, not a
16
+ * validated enum, so there is nothing to reject against.
17
+ *
18
+ * @param rows - Rows produced by the pack's list query (post-filter).
19
+ * @param search - The declared search (absent means no searching).
20
+ * @param params - Story parameters as provided by the surface.
21
+ * @returns Rows containing the term in at least one searched variable.
22
+ */
23
+ export default function applyPackSearch(
24
+ rows: Record<string, string>[],
25
+ search: StoryPackSearch | undefined,
26
+ params: Record<string, unknown>,
27
+ ): Record<string, string>[] {
28
+ const provided = params.search;
29
+ if (search === undefined || typeof provided !== "string") {
30
+ return rows;
31
+ }
32
+ const term = provided.trim().normalize("NFC").toLowerCase();
33
+ if (term === "") {
34
+ return rows;
35
+ }
36
+ return rows.filter((row) =>
37
+ search.variables.some((variable) =>
38
+ row[variable]?.normalize("NFC").toLowerCase().includes(term),
39
+ ),
40
+ );
41
+ }
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import buildLookupQuery, {
3
+ buildLookupByIriQuery,
3
4
  buildLookupNamesQuery,
4
5
  escapeSparqlString,
5
6
  formatTerm,
@@ -41,6 +42,35 @@ describe("buildLookupQuery", () => {
41
42
  });
42
43
  });
43
44
 
45
+ describe("buildLookupByIriQuery", () => {
46
+ it("binds the resolved IRI instead of filtering on the name", () => {
47
+ const query = buildLookupByIriQuery(
48
+ {
49
+ by: "ex:name",
50
+ type: "ex:Recipe",
51
+ fields: [{ name: "category", property: "ex:category" }],
52
+ },
53
+ "http://example.org/recipes/pancakes",
54
+ );
55
+ expect(query).toContain(
56
+ "BIND(<http://example.org/recipes/pancakes> AS ?uri)",
57
+ );
58
+ expect(query).toContain("?uri ex:name ?name .");
59
+ expect(query).toContain("?uri a ex:Recipe .");
60
+ expect(query).toContain("OPTIONAL { ?uri ex:category ?category . }");
61
+ expect(query).not.toContain("FILTER");
62
+ expect(query).toContain("LIMIT 1");
63
+ });
64
+
65
+ it("omits the type constraint when absent", () => {
66
+ const query = buildLookupByIriQuery(
67
+ { by: "ex:name" },
68
+ "http://example.org/x",
69
+ );
70
+ expect(query).not.toContain("?uri a ");
71
+ });
72
+ });
73
+
44
74
  describe("buildLookupNamesQuery", () => {
45
75
  it("selects all names for the by property", () => {
46
76
  const query = buildLookupNamesQuery({ by: "ex:name", type: "ex:Recipe" });
@@ -1,4 +1,4 @@
1
- import type { StoryPackLookup } from "./types.js";
1
+ import type { StoryPackExpand, StoryPackLookup } from "./types.js";
2
2
 
3
3
  /**
4
4
  * Build the generated, injection-safe lookup queries for a pack story.
@@ -59,6 +59,80 @@ export default function buildLookupQuery(
59
59
  .join("\n");
60
60
  }
61
61
 
62
+ /**
63
+ * Build the SELECT retrieving one entity addressed directly by IRI.
64
+ *
65
+ * Mirrors {@link buildLookupQuery} but binds the already-resolved IRI to
66
+ * `?uri` instead of filtering on the `by` property's value, so absolute
67
+ * IRIs and prefixed names address an entity exactly. Injection-safe by
68
+ * construction: the caller resolves and validates `iri` against the
69
+ * embeddable-IRI shape (no whitespace or `<>"` characters) BEFORE it is
70
+ * interpolated here — raw user input never reaches this function.
71
+ *
72
+ * @param lookup - The pack's lookup declaration.
73
+ * @param iri - A resolved, validated absolute IRI.
74
+ * @returns SPARQL SELECT text.
75
+ */
76
+ export function buildLookupByIriQuery(
77
+ lookup: StoryPackLookup,
78
+ iri: string,
79
+ ): string {
80
+ const fields = [...(lookup.fields ?? []), ...(lookup.sections ?? [])];
81
+ const vars = fields.map((field) => `?${field.name}`).join(" ");
82
+ const optionals = fields
83
+ .map(
84
+ (field) =>
85
+ ` OPTIONAL { ?uri ${formatTerm(field.property)} ?${field.name} . }`,
86
+ )
87
+ .join("\n");
88
+ const typeConstraint = lookup.type
89
+ ? ` ?uri a ${formatTerm(lookup.type)} .\n`
90
+ : "";
91
+
92
+ return [
93
+ `SELECT ?uri ?name${vars.length > 0 ? ` ${vars}` : ""} WHERE {`,
94
+ ` BIND(<${iri}> AS ?uri)`,
95
+ ` ?uri ${formatTerm(lookup.by)} ?name .`,
96
+ typeConstraint + optionals,
97
+ "}",
98
+ "LIMIT 1",
99
+ ]
100
+ .filter((line) => line !== "")
101
+ .join("\n");
102
+ }
103
+
104
+ /**
105
+ * Build the sub-SELECT retrieving one expand's child rows for a resolved
106
+ * entity.
107
+ *
108
+ * Injection-safe by construction: the only interpolated value is
109
+ * `entityUri`, which is the IRI the base lookup already resolved from the
110
+ * store (never user input), and the relation/properties are validated pack
111
+ * terms. Returns one row per child node, each carrying the declared fields.
112
+ *
113
+ * @param expand - The expand declaration.
114
+ * @param entityUri - The resolved entity IRI (from the base lookup's `?uri`).
115
+ * @returns SPARQL SELECT text yielding one row per child node.
116
+ */
117
+ export function buildExpandQuery(
118
+ expand: StoryPackExpand,
119
+ entityUri: string,
120
+ ): string {
121
+ const vars = expand.select.map((field) => `?${field.name}`).join(" ");
122
+ const optionals = expand.select
123
+ .map(
124
+ (field) =>
125
+ ` OPTIONAL { ?child ${formatTerm(field.property)} ?${field.name} . }`,
126
+ )
127
+ .join("\n");
128
+ return [
129
+ `SELECT ${vars} WHERE {`,
130
+ ` <${entityUri}> ${formatTerm(expand.relation)} ?child .`,
131
+ optionals,
132
+ "}",
133
+ ].join("\n");
134
+ }
135
+
62
136
  /**
63
137
  * Build the SELECT listing all entity names — lookup-miss suggestions.
64
138
  *
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Bundled transitional story packs.
3
+ *
4
+ * These are declarative replacements for design-system domains that used to be
5
+ * hand-written TypeScript in the CLI core. They ship inside pragma during the
6
+ * strangler migration (ADR C.07) and are the **lowest-precedence** pack
7
+ * source: a `pragma.config.json` `stories` entry or a semantic package's
8
+ * `stories/*.json` for the same noun overrides the bundled one, so a package
9
+ * can take ownership of its noun (the P4 end state) without a code change here.
10
+ *
11
+ * As each leaf domain is cut over, its built-in command is deleted (freeing the
12
+ * noun from the reserved set) and its pack is added to this list.
13
+ */
14
+
15
+ import type { StoryPackDefinition } from "../types.js";
16
+ import { standardPack } from "./standardPack.js";
17
+ import { tierPack } from "./tierPack.js";
18
+
19
+ /** Every pack pragma ships by default, lowest precedence. */
20
+ export const BUNDLED_PACKS: readonly StoryPackDefinition[] = [
21
+ tierPack,
22
+ standardPack,
23
+ ];
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Bundled transitional `standard` pack.
3
+ *
4
+ * Replaces the hand-written `standard` domain (~1,300 LOC of command/
5
+ * operation/formatter/orchestration/MCP wiring) with a declarative pack
6
+ * definition. `standard list|lookup|categories|sample` and the
7
+ * `standard_list|_lookup|_categories|_sample` MCP tools compile from this
8
+ * definition through the same kernel as every other pack, so the CLI core
9
+ * stops carrying standard-specific TypeScript.
10
+ *
11
+ * @remarks transitional — this ships inside pragma today; in P4 it moves
12
+ * into the `@canonical/code-standards` package as `stories/standard.json`
13
+ * alongside that package's prefix declaration, at which point the core
14
+ * carries no standard at all.
15
+ *
16
+ * Scope notes (semantic parity, not byte parity — see standardParity.test):
17
+ * - Rows/entities are the uniform pack shapes: the list SELECT binds the
18
+ * standard IRI to `uri` and renames `categoryName` → `category`, so JSON
19
+ * carries the same keys as the old summary items.
20
+ * - The old domain's `--digest`/`--detailed` LIST enrichment is not
21
+ * reproduced (pack lists have a single level); the same data is reachable
22
+ * through `standard lookup --detail …` and `standard sample`.
23
+ * - Lookup disclosure keeps three levels. The old digest's bespoke
24
+ * "first do-example truncated to 120 chars" rendering is not expressible
25
+ * generically; here `digest` gates the full `dos` expand (richer than the
26
+ * old digest) and `detailed` adds `donts`.
27
+ * - `extends` stays the raw IRI in JSON (renderers compact it at display
28
+ * time); the old domain compacted it in resolved data.
29
+ */
30
+
31
+ import type { StoryPackDefinition } from "../types.js";
32
+
33
+ /**
34
+ * The `standard` read stories as data.
35
+ *
36
+ * The list SELECT mirrors the query the old `listStandards` operation sent
37
+ * the store — `cs:name` and the category path stay OPTIONAL so a standard
38
+ * without a display name or category still appears — while `ORDER BY ?name`
39
+ * sorts by the human-facing name. The BIND reproduces the old
40
+ * `deriveStandardName` fallback declaratively: a standard without `cs:name`
41
+ * displays its IRI fragment with dots rendered as slashes
42
+ * (`…#react.component.props` → `react/component/props`). Category filtering
43
+ * is value-free (the category set lives in the graph) and search covers
44
+ * name + description; both are post-query row predicates, so user input
45
+ * never touches this query text.
46
+ */
47
+ export const standardPack: StoryPackDefinition = {
48
+ noun: "standard",
49
+ description: "List all code standards",
50
+ toolDescription:
51
+ "List code standards. Optionally filter by category or search term.",
52
+ list: {
53
+ query: [
54
+ "SELECT ?uri ?name ?category ?description",
55
+ "WHERE {",
56
+ " ?uri a cs:CodeStandard ;",
57
+ " cs:description ?description .",
58
+ " OPTIONAL { ?uri cs:name ?n . }",
59
+ ' BIND(COALESCE(?n, REPLACE(STRAFTER(STR(?uri), "#"), "\\\\.", "/")) AS ?name)',
60
+ " OPTIONAL {",
61
+ " ?uri cs:hasCategory ?cat .",
62
+ " ?cat cs:slug ?category .",
63
+ " }",
64
+ "}",
65
+ "ORDER BY ?name",
66
+ ].join("\n"),
67
+ columns: [
68
+ { field: "uri", label: "IRI" },
69
+ { field: "name", label: "Name" },
70
+ { field: "category", label: "Category" },
71
+ { field: "description", label: "Description" },
72
+ ],
73
+ filters: [
74
+ {
75
+ param: "category",
76
+ variable: "category",
77
+ description: "Filter by category name",
78
+ },
79
+ ],
80
+ search: {
81
+ variables: ["name", "description"],
82
+ description: "Search in name and description",
83
+ },
84
+ },
85
+ verbs: [
86
+ {
87
+ verb: "categories",
88
+ description: "List all standard categories with counts",
89
+ toolDescription: "List all code standard categories.",
90
+ // OPTIONAL keeps zero-count categories in the result (a category with
91
+ // no standards still lists, with count 0), mirroring the old
92
+ // listCategories operation.
93
+ query: [
94
+ "SELECT ?name (COUNT(?standard) AS ?count)",
95
+ "WHERE {",
96
+ " ?cat a cs:Category ;",
97
+ " cs:slug ?name .",
98
+ " OPTIONAL {",
99
+ " ?standard a cs:CodeStandard ;",
100
+ " cs:hasCategory ?cat .",
101
+ " }",
102
+ "}",
103
+ "GROUP BY ?name",
104
+ "ORDER BY ?name",
105
+ ].join("\n"),
106
+ columns: [
107
+ { field: "name", label: "Category" },
108
+ { field: "count", label: "Standards" },
109
+ ],
110
+ },
111
+ ],
112
+ lookup: {
113
+ by: "cs:name",
114
+ type: "cs:CodeStandard",
115
+ description:
116
+ "Look up detailed information for a standard by name, IRI, or glob",
117
+ toolDescription:
118
+ "Get detailed information about one or more code standards including dos and donts with code examples. Address standards by name, prefixed name (cs:…), absolute IRI, or glob pattern (react/component/*).",
119
+ fields: [
120
+ {
121
+ name: "category",
122
+ property: "cs:hasCategory/cs:slug",
123
+ label: "Category",
124
+ },
125
+ {
126
+ name: "description",
127
+ property: "cs:description",
128
+ label: "Description",
129
+ },
130
+ { name: "extends", property: "cs:extends", label: "Extends" },
131
+ ],
132
+ expand: [
133
+ {
134
+ name: "dos",
135
+ heading: "Do",
136
+ relation: "cs:do",
137
+ select: [
138
+ { name: "caption", property: "cs:description" },
139
+ { name: "language", property: "cs:language" },
140
+ { name: "code", property: "cs:code" },
141
+ ],
142
+ level: "digest",
143
+ },
144
+ {
145
+ name: "donts",
146
+ heading: "Don't",
147
+ relation: "cs:dont",
148
+ select: [
149
+ { name: "caption", property: "cs:description" },
150
+ { name: "language", property: "cs:language" },
151
+ { name: "code", property: "cs:code" },
152
+ ],
153
+ level: "detailed",
154
+ },
155
+ ],
156
+ disclosure: { levels: ["summary", "digest", "detailed"] },
157
+ sample: {
158
+ description:
159
+ "Return randomly selected complete standard instances as exemplars for shape discovery",
160
+ toolDescription:
161
+ "Return 1–5 randomly selected complete code standard instances as exemplars. Use BEFORE writing queries to see actual data shapes, property names, and value formats. Each call returns different instances.",
162
+ },
163
+ },
164
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Bundled transitional `tier` pack.
3
+ *
4
+ * Replaces the hand-written `tier` domain (~179 LOC of command/operation/
5
+ * formatter/MCP wiring) with a declarative list story. `tier list` and the
6
+ * `tier_list` MCP tool are compiled from this definition through the same
7
+ * kernel as every other pack, so the CLI core stops carrying tier-specific
8
+ * TypeScript.
9
+ *
10
+ * @remarks transitional — this ships inside pragma today; in P4 it moves into
11
+ * the `@canonical/design-system` package as `stories/tier.json` alongside that
12
+ * package's prefix declaration, at which point the core carries no tier at all.
13
+ *
14
+ * Scope note: the old domain's `TierEntry.depth`/`parent` fields were always
15
+ * `0`/absent — tier hierarchy is encoded in the slash-separated *path string*
16
+ * (`apps/lxd`), not in graph edges, and the ordered-inheritance logic lives in
17
+ * `buildTierFilter`/`resolveTierChain` (a filter primitive other domains use).
18
+ * `tier list` is therefore a flat, name-ordered list, which this pack
19
+ * reproduces faithfully; there is no graph tree to project.
20
+ */
21
+
22
+ import type { StoryPackDefinition } from "../types.js";
23
+
24
+ /**
25
+ * The `tier` list story as data.
26
+ *
27
+ * The SELECT binds the tier IRI to `uri` and its `ds:name` to `name` (the
28
+ * honest mapping of the `ds:name` property), so `--format json` is the uniform
29
+ * `{uri, name}` pack shape and the generic renderer emits `- ` + "`ds:apps`" +
30
+ * ` — **Apps**` in llm mode. The store injects PREFIX declarations, so `ds:`
31
+ * resolves against the merged prefix map. The old domain's always-zero
32
+ * `depth` field is intentionally dropped.
33
+ */
34
+ export const tierPack: StoryPackDefinition = {
35
+ noun: "tier",
36
+ description: "List all tiers in the design system ontology",
37
+ toolDescription: "List all tiers in the design-system ontology.",
38
+ list: {
39
+ query: [
40
+ "SELECT ?uri ?name WHERE {",
41
+ " ?uri a ds:Tier ;",
42
+ " ds:name ?name .",
43
+ "} ORDER BY ?name",
44
+ ].join("\n"),
45
+ columns: [
46
+ { field: "uri", label: "IRI" },
47
+ { field: "name", label: "Name" },
48
+ ],
49
+ },
50
+ };