@canonical/pragma-cli 0.27.0 → 0.29.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 (169) hide show
  1. package/dist/pragma +0 -0
  2. package/package.json +19 -17
  3. package/src/config/readConfig.ts +38 -2
  4. package/src/config/types.ts +12 -1
  5. package/src/config/writeConfig.ts +12 -0
  6. package/src/constants.ts +12 -1
  7. package/src/domains/block/commands/index.ts +2 -1
  8. package/src/domains/block/commands/sample.ts +55 -0
  9. package/src/domains/block/formatters/index.ts +2 -1
  10. package/src/domains/block/formatters/sample.ts +21 -0
  11. package/src/domains/block/index.ts +3 -3
  12. package/src/domains/block/mcp/specs.ts +48 -0
  13. package/src/domains/block/operations/index.ts +2 -1
  14. package/src/domains/block/operations/lookup.ts +9 -0
  15. package/src/domains/block/operations/sample.ts +41 -0
  16. package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
  17. package/src/domains/block/orchestration/resolveBlockLookup.ts +10 -3
  18. package/src/domains/config/commands/framework.test.ts +88 -0
  19. package/src/domains/config/commands/framework.ts +85 -0
  20. package/src/domains/config/commands/index.ts +2 -0
  21. package/src/domains/config/commands/trace.test.ts +148 -0
  22. package/src/domains/config/commands/trace.ts +80 -0
  23. package/src/domains/config/formatters/framework.ts +34 -0
  24. package/src/domains/config/formatters/index.ts +2 -0
  25. package/src/domains/config/formatters/trace.ts +28 -0
  26. package/src/domains/config/index.ts +15 -2
  27. package/src/domains/config/operations/index.ts +1 -0
  28. package/src/domains/config/operations/validateFramework.ts +24 -0
  29. package/src/domains/config/tasks/index.ts +2 -0
  30. package/src/domains/config/tasks/setChannel.ts +7 -1
  31. package/src/domains/config/tasks/setFramework.ts +40 -0
  32. package/src/domains/config/tasks/setTier.ts +7 -1
  33. package/src/domains/config/tasks/setTrace.ts +38 -0
  34. package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
  35. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +35 -41
  36. package/src/domains/doctor/operations/checks.test.ts +4 -1
  37. package/src/domains/doctor/operations/runChecks.test.ts +72 -0
  38. package/src/domains/doctor/operations/runChecks.ts +48 -12
  39. package/src/domains/graphql/commands/build.test.ts +194 -0
  40. package/src/domains/graphql/commands/build.ts +125 -0
  41. package/src/domains/graphql/commands/check.test.ts +139 -0
  42. package/src/domains/graphql/commands/check.ts +87 -0
  43. package/src/domains/graphql/commands/index.ts +4 -0
  44. package/src/domains/graphql/formatters/index.ts +3 -0
  45. package/src/domains/graphql/formatters/report.ts +78 -0
  46. package/src/domains/graphql/helpers/index.ts +4 -0
  47. package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
  48. package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
  49. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
  50. package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
  51. package/src/domains/graphql/index.ts +21 -0
  52. package/src/domains/graphql/operations/compileSchema.test.ts +119 -0
  53. package/src/domains/graphql/operations/compileSchema.ts +109 -0
  54. package/src/domains/graphql/operations/index.ts +7 -0
  55. package/src/domains/graphql/types.ts +29 -0
  56. package/src/domains/info/operations/collectInfo.test.ts +2 -2
  57. package/src/domains/info/operations/collectInfo.ts +4 -4
  58. package/src/domains/llm/capabilities.test.ts +91 -0
  59. package/src/domains/llm/commands/capabilities.ts +28 -0
  60. package/src/domains/llm/commands/index.ts +1 -0
  61. package/src/domains/llm/data/commandReference.ts +90 -18
  62. package/src/domains/llm/data/decisionTrees.ts +15 -13
  63. package/src/domains/llm/data/index.ts +2 -0
  64. package/src/domains/llm/data/toolCatalog.ts +184 -0
  65. package/src/domains/llm/formatters/capabilities.ts +50 -0
  66. package/src/domains/llm/formatters/index.ts +1 -0
  67. package/src/domains/llm/index.ts +3 -2
  68. package/src/domains/llm/mcp/index.ts +1 -1
  69. package/src/domains/llm/mcp/specs.ts +59 -52
  70. package/src/domains/llm/types.ts +40 -0
  71. package/src/domains/modifier/commands/index.ts +2 -1
  72. package/src/domains/modifier/commands/sample.ts +53 -0
  73. package/src/domains/modifier/formatters/index.ts +2 -1
  74. package/src/domains/modifier/formatters/sample.ts +15 -0
  75. package/src/domains/modifier/index.ts +7 -3
  76. package/src/domains/modifier/mcp/specs.ts +47 -0
  77. package/src/domains/modifier/operations/index.ts +2 -1
  78. package/src/domains/modifier/operations/lookup.ts +9 -0
  79. package/src/domains/modifier/operations/sample.ts +32 -0
  80. package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
  81. package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
  82. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +14 -2
  83. package/src/domains/refs/operations/index.ts +1 -1
  84. package/src/domains/refs/operations/paths.ts +29 -1
  85. package/src/domains/refs/operations/updateRefs.test.ts +6 -3
  86. package/src/domains/refs/operations/updateRefs.ts +3 -2
  87. package/src/domains/shared/bootStore.ts +88 -49
  88. package/src/domains/shared/contracts.ts +1 -0
  89. package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
  90. package/src/domains/shared/loaders/bundledLoader.ts +108 -0
  91. package/src/domains/shared/loaders/gitLoader.ts +31 -0
  92. package/src/domains/shared/loaders/index.ts +6 -0
  93. package/src/domains/shared/loaders/localLoader.ts +73 -0
  94. package/src/domains/shared/loaders/readPackageDir.ts +112 -0
  95. package/src/domains/shared/lookupMany.ts +9 -1
  96. package/src/domains/shared/packages.ts +21 -120
  97. package/src/domains/shared/pickRandom.ts +22 -0
  98. package/src/domains/shared/runtime.ts +6 -3
  99. package/src/domains/shared/sampleFormatters.ts +47 -0
  100. package/src/domains/shared/semanticPackage.ts +96 -0
  101. package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
  102. package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
  103. package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
  104. package/src/domains/shared/suggestions/expandGlob.ts +51 -0
  105. package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
  106. package/src/domains/shared/suggestions/index.ts +7 -0
  107. package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
  108. package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
  109. package/src/domains/shared/suggestions/suggestNames.ts +97 -0
  110. package/src/domains/shared/types/index.ts +1 -0
  111. package/src/domains/shared/types/runtime.ts +4 -1
  112. package/src/domains/shared/types/sample.ts +17 -0
  113. package/src/domains/skill/helpers/resolveSkillSources.ts +49 -16
  114. package/src/domains/standard/commands/index.ts +2 -1
  115. package/src/domains/standard/commands/sample.ts +57 -0
  116. package/src/domains/standard/formatters/index.ts +2 -1
  117. package/src/domains/standard/formatters/sample.ts +15 -0
  118. package/src/domains/standard/index.ts +8 -1
  119. package/src/domains/standard/mcp/specs.ts +48 -1
  120. package/src/domains/standard/operations/index.ts +2 -1
  121. package/src/domains/standard/operations/lookup.ts +9 -0
  122. package/src/domains/standard/operations/sample.ts +35 -0
  123. package/src/domains/standard/orchestration/resolveStandardLookup.ts +14 -2
  124. package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
  125. package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
  126. package/src/domains/token/commands/index.ts +2 -1
  127. package/src/domains/token/commands/sample.ts +53 -0
  128. package/src/domains/token/formatters/index.ts +2 -1
  129. package/src/domains/token/formatters/sample.ts +16 -0
  130. package/src/domains/token/index.ts +8 -1
  131. package/src/domains/token/mcp/specs.ts +47 -1
  132. package/src/domains/token/operations/index.ts +2 -1
  133. package/src/domains/token/operations/lookup.ts +9 -0
  134. package/src/domains/token/operations/sample.ts +32 -0
  135. package/src/domains/token/orchestration/resolveTokenLookup.ts +14 -2
  136. package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
  137. package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
  138. package/src/domains/trace/commands/clear.ts +71 -0
  139. package/src/domains/trace/commands/index.ts +5 -0
  140. package/src/domains/trace/commands/sessions.ts +32 -0
  141. package/src/domains/trace/commands/show.ts +145 -0
  142. package/src/domains/trace/formatters/index.ts +9 -0
  143. package/src/domains/trace/formatters/sessions.ts +80 -0
  144. package/src/domains/trace/formatters/show.ts +122 -0
  145. package/src/domains/trace/index.ts +17 -0
  146. package/src/domains/trace/operations/index.ts +8 -0
  147. package/src/domains/trace/operations/listSessions.test.ts +55 -0
  148. package/src/domains/trace/operations/listSessions.ts +52 -0
  149. package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
  150. package/src/domains/trace/operations/readTraceLog.ts +104 -0
  151. package/src/domains/trace/tracePlugin.test.ts +171 -0
  152. package/src/domains/trace/tracePlugin.ts +159 -0
  153. package/src/domains/trace/types.ts +65 -0
  154. package/src/error/PragmaError.ts +15 -2
  155. package/src/error/types.ts +15 -1
  156. package/src/mcp/error/serializeErrorPayload.ts +7 -0
  157. package/src/mcp/error/types.ts +8 -0
  158. package/src/mcp/registerResources.test.ts +38 -16
  159. package/src/mcp/registerResources.ts +57 -8
  160. package/src/mcp/tools/registerTools.test.ts +22 -7
  161. package/src/mcp/utils/wrapTool.test.ts +1 -0
  162. package/src/pipeline/collectCommands.ts +7 -1
  163. package/src/pipeline/renderError.ts +15 -0
  164. package/src/pipeline/resolveCommandKind.ts +8 -1
  165. package/src/pipeline/runCli.ts +12 -1
  166. package/src/testing/createTestMcpClient.ts +1 -0
  167. package/src/testing/graphqlFixtures.ts +72 -0
  168. package/src/testing/index.ts +7 -0
  169. package/src/testing/integration/mcp-surface.test.ts +11 -5
@@ -157,8 +157,11 @@ describe("updateRefs", () => {
157
157
 
158
158
  it("uses defaults when no config exists", async () => {
159
159
  const results = await updateRefs({ cwd: projectDir });
160
- // All 4 defaults are npm, so all skipped
161
- expect(results.every((r) => r.kind === "skipped")).toBe(true);
162
- expect(results).toHaveLength(4);
160
+ // All 3 defaults are git refs
161
+ expect(results).toHaveLength(3);
162
+ for (const r of results) {
163
+ // Each result is either cloned, up-to-date, or error (network)
164
+ expect(["cloned", "updated", "up-to-date", "error"]).toContain(r.kind);
165
+ }
163
166
  });
164
167
  });
@@ -152,8 +152,9 @@ function mergeEntries(
152
152
 
153
153
  const merged = new Map<string, RawPackageEntry>();
154
154
 
155
- for (const pkg of DEFAULT_PACKAGES) {
156
- merged.set(pkg, pkg);
155
+ for (const entry of DEFAULT_PACKAGES) {
156
+ const name = typeof entry === "string" ? entry : entry.name;
157
+ merged.set(name, entry);
157
158
  }
158
159
  for (const entry of globalEntries) {
159
160
  const name = typeof entry === "string" ? entry : entry.name;
@@ -1,85 +1,124 @@
1
1
  /**
2
- * Boot a ke store from pragma.config.json sources.
2
+ * Boot a ke store from resolved semantic packages.
3
3
  *
4
- * Reads configuration, resolves sources (defaults or overridden),
5
- * creates the store with registered prefixes, and wraps errors.
4
+ * Resolves packages via the loader chain (local > git > bundled),
5
+ * then loads all graph content into the store.
6
6
  *
7
7
  * @note Impure — reads filesystem, creates ke store.
8
8
  */
9
9
 
10
- import { existsSync } from "node:fs";
11
- import { join } from "node:path";
12
- import type { SourceSpec, Store } from "@canonical/ke";
13
- import { createStore } from "@canonical/ke";
10
+ import {
11
+ createStore,
12
+ definePlugin,
13
+ type Plugin,
14
+ type SourceSpec,
15
+ type Store,
16
+ } from "@canonical/ke";
14
17
  import { PragmaError } from "../../error/index.js";
15
18
  import type { PackageRef } from "../refs/operations/parseRef.js";
16
- import { resolvePackages } from "./packages.js";
19
+ import { parsePackageEntry } from "../refs/operations/parseRef.js";
20
+ import {
21
+ createBundledLoader,
22
+ createGitLoader,
23
+ createLocalLoader,
24
+ } from "./loaders/index.js";
25
+ import { DEFAULT_PACKAGES } from "./packages.js";
17
26
  import { PREFIX_MAP } from "./prefixes.js";
18
-
19
- /**
20
- * Convention-based TTL directories to scan in each package.
21
- * Only directories that exist are globbed — packages that lack
22
- * a `data/` or `definitions/` dir are silently skipped.
23
- */
24
- const TTL_DIRS: readonly { dir: string; glob: string }[] = [
25
- { dir: "definitions", glob: "definitions/**/*.ttl" },
26
- { dir: "data", glob: "data/**/*.ttl" },
27
- ];
28
-
29
- /**
30
- * Resolve default TTL sources from resolved packages.
31
- *
32
- * Uses convention-based TTL discovery: definitions and data globs
33
- * relative to each package root. Only adds globs for directories
34
- * that actually exist in the package.
35
- *
36
- * @param refs - Parsed package references. Omit for defaults.
37
- */
38
- export function defaultSources(refs?: ReadonlyArray<PackageRef>): SourceSpec[] {
39
- const sources: SourceSpec[] = [];
40
- const resolved = resolvePackages(refs);
41
-
42
- for (const { dir } of resolved) {
43
- for (const entry of TTL_DIRS) {
44
- if (existsSync(join(dir, entry.dir))) {
45
- sources.push(join(dir, entry.glob));
46
- }
47
- }
48
- }
49
-
50
- return sources;
51
- }
27
+ import type { GraphContent, SemanticPackage } from "./semanticPackage.js";
28
+ import { resolveSemanticPackages } from "./semanticPackage.js";
52
29
 
53
30
  export interface BootStoreOptions {
54
- /** Override sources (skip filesystem resolution). */
31
+ /** Override sources (skip package resolution — for testing). */
55
32
  sources?: SourceSpec[];
56
33
  /** Working directory for resolving relative paths. */
57
34
  cwd?: string;
58
35
  /** Cache path for serialized store. */
59
36
  cache?: string;
60
- /** Parsed package references for ref-based resolution. */
37
+ /** Parsed package references for loader-based resolution. */
61
38
  refs?: ReadonlyArray<PackageRef>;
39
+ /** Enable query tracing (from config). Env var PRAGMA_TRACE=1 overrides. */
40
+ trace?: boolean;
41
+ }
42
+
43
+ export interface BootResult {
44
+ /** The initialized ke store. */
45
+ readonly store: Store;
46
+ /** Resolved semantic packages (empty when sources override is used). */
47
+ readonly packages: readonly SemanticPackage[];
62
48
  }
63
49
 
64
50
  /**
65
51
  * Boot a ke store from configuration.
66
52
  *
53
+ * When `sources` is provided, uses them directly (testing path).
54
+ * Otherwise, resolves packages via the loader chain and loads their
55
+ * graph content into the store.
56
+ *
67
57
  * @throws PragmaError with code STORE_ERROR on failure.
68
58
  */
69
59
  export async function bootStore(
70
60
  options: BootStoreOptions = {},
71
- ): Promise<Store> {
72
- const sources = options.sources ?? defaultSources(options.refs);
73
-
61
+ ): Promise<BootResult> {
74
62
  try {
63
+ // Testing/programmatic override — use explicit sources
64
+ if (options.sources) {
65
+ const store = await createStore({
66
+ sources: options.sources,
67
+ prefixes: PREFIX_MAP,
68
+ cache: options.cache,
69
+ cwd: options.cwd,
70
+ });
71
+ return { store, packages: [] };
72
+ }
73
+
74
+ // Resolve packages via loader chain: local > git > bundled
75
+ const refs =
76
+ options.refs && options.refs.length > 0
77
+ ? options.refs
78
+ : DEFAULT_PACKAGES.map(parsePackageEntry);
79
+ // Precedence: local (file:// + node_modules) > git cache > bundled
80
+ const loaders = [
81
+ createLocalLoader(),
82
+ createGitLoader(),
83
+ createBundledLoader(),
84
+ ];
85
+ const packages = await resolveSemanticPackages(refs, loaders);
86
+
87
+ // Collect all graph content from resolved packages
88
+ const allGraphs = packages.flatMap((pkg) => pkg.graphs);
89
+
90
+ // Use a ke plugin to load graph content during store creation.
91
+ // The onReady hook has access to ctx.load() which loads raw content.
92
+ const graphLoaderPlugin = definePlugin({
93
+ name: "pragma-graph-loader",
94
+ onReady(ctx) {
95
+ for (const graph of allGraphs) {
96
+ ctx.load(graph.content, { format: graph.format });
97
+ }
98
+ },
99
+ });
100
+
101
+ // biome-ignore lint: Plugin generic variance requires explicit unknown
102
+ const plugins: Plugin<any>[] = [graphLoaderPlugin];
103
+ const traceEnabled =
104
+ process.env.PRAGMA_TRACE === "1" || options.trace === true;
105
+ if (traceEnabled) {
106
+ const { createTracePlugin } = await import("../trace/tracePlugin.js");
107
+ const { traceDir } = await import("../refs/operations/paths.js");
108
+ plugins.push(createTracePlugin({ traceDir: traceDir() }));
109
+ }
110
+
75
111
  const store = await createStore({
76
- sources,
112
+ sources: [],
77
113
  prefixes: PREFIX_MAP,
78
114
  cache: options.cache,
79
115
  cwd: options.cwd,
116
+ plugins,
80
117
  });
81
- return store;
118
+
119
+ return { store, packages };
82
120
  } catch (error) {
121
+ if (error instanceof PragmaError) throw error;
83
122
  throw PragmaError.storeError(
84
123
  error instanceof Error ? error.message : String(error),
85
124
  {
@@ -56,6 +56,7 @@ export interface LookupResult<T> {
56
56
  query: string;
57
57
  code: string;
58
58
  message: string;
59
+ suggestions?: readonly string[];
59
60
  }[];
60
61
  }
61
62
 
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { PragmaError } from "#error";
3
+ import blockSpecs from "../block/mcp/specs.js";
4
+ import blockEmptyError from "../block/orchestration/blockEmptyError.js";
5
+ import modifierSpecs from "../modifier/mcp/specs.js";
6
+ import modifierEmptyError from "../modifier/orchestration/modifierEmptyError.js";
7
+ import standardSpecs from "../standard/mcp/specs.js";
8
+ import standardEmptyError from "../standard/orchestration/standardEmptyError.js";
9
+ import tokenSpecs from "../token/mcp/specs.js";
10
+ import tokenEmptyError from "../token/orchestration/tokenEmptyError.js";
11
+ import type { FilterConfig } from "./types/index.js";
12
+
13
+ const filters: FilterConfig = { tier: "Apps/WPE", channel: "normal" };
14
+ const allTiers: FilterConfig = { tier: undefined, channel: "normal" };
15
+
16
+ /**
17
+ * Every distinct empty-result scenario each domain can produce, paired with the
18
+ * set of MCP tool names that domain actually registers. The invariant below
19
+ * holds the four hints to the S-grade contract so authored consistency cannot
20
+ * silently re-drift.
21
+ */
22
+ const cases: ReadonlyArray<{
23
+ domain: string;
24
+ error: PragmaError;
25
+ toolNames: ReadonlySet<string>;
26
+ // store-empty (packages absent) → the install rung, which must NOT carry an
27
+ // mcp retry (re-listing an absent store loops). filter-narrowing → may carry
28
+ // a list-retry mcp pointer.
29
+ storeEmpty: boolean;
30
+ }> = [
31
+ {
32
+ domain: "block (filter active)",
33
+ error: blockEmptyError(filters, false),
34
+ toolNames: new Set(blockSpecs.map((s) => s.name)),
35
+ storeEmpty: false,
36
+ },
37
+ {
38
+ domain: "block (all tiers — terminal)",
39
+ error: blockEmptyError(allTiers, true),
40
+ toolNames: new Set(blockSpecs.map((s) => s.name)),
41
+ storeEmpty: false,
42
+ },
43
+ {
44
+ domain: "token (category active)",
45
+ error: tokenEmptyError("color"),
46
+ toolNames: new Set(tokenSpecs.map((s) => s.name)),
47
+ storeEmpty: false,
48
+ },
49
+ {
50
+ domain: "token (store-empty)",
51
+ error: tokenEmptyError(),
52
+ toolNames: new Set(tokenSpecs.map((s) => s.name)),
53
+ storeEmpty: true,
54
+ },
55
+ {
56
+ domain: "modifier (store-empty)",
57
+ error: modifierEmptyError(),
58
+ toolNames: new Set(modifierSpecs.map((s) => s.name)),
59
+ storeEmpty: true,
60
+ },
61
+ {
62
+ domain: "standard (filter active)",
63
+ error: standardEmptyError({ category: "react" }),
64
+ toolNames: new Set(standardSpecs.map((s) => s.name)),
65
+ storeEmpty: false,
66
+ },
67
+ {
68
+ domain: "standard (store-empty)",
69
+ error: standardEmptyError({}),
70
+ toolNames: new Set(standardSpecs.map((s) => s.name)),
71
+ storeEmpty: true,
72
+ },
73
+ ];
74
+
75
+ describe("empty-error recovery contract (cross-domain)", () => {
76
+ it.each(cases)("$domain honours the S-grade recovery shape", ({
77
+ error,
78
+ toolNames,
79
+ storeEmpty,
80
+ }) => {
81
+ const recovery = error.recovery;
82
+
83
+ // Terminal state (e.g. block already at all-tiers) is allowed.
84
+ if (recovery === undefined) return;
85
+
86
+ // Every hint carries a human-readable message.
87
+ expect(recovery.message).toBeTruthy();
88
+
89
+ // No hint is message-only: a runnable cli is always present (either a
90
+ // pragma re-list for filter-narrowing, or a bun add for store-empty).
91
+ expect(recovery.cli).toBeTruthy();
92
+
93
+ if (storeEmpty) {
94
+ // Store-empty must NOT carry an mcp retry: re-listing an absent store
95
+ // just loops. This is the anti-drift guard for the install rung.
96
+ expect(recovery.mcp).toBeUndefined();
97
+ } else if (recovery.mcp !== undefined) {
98
+ // A filter-narrowing mcp pointer must name a tool the domain actually
99
+ // registers — catches renamed/dangling tool references.
100
+ expect(toolNames.has(recovery.mcp.tool)).toBe(true);
101
+ }
102
+ });
103
+
104
+ it("never points an install hint at @canonical/ds-global (not a real default)", () => {
105
+ for (const { error } of cases) {
106
+ expect(error.recovery?.cli ?? "").not.toContain("ds-global");
107
+ }
108
+ });
109
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Bundled package loader for compiled binaries.
3
+ *
4
+ * Resolves semantic content from `Bun.embeddedFiles` — TTL and skill
5
+ * files embedded into the compiled binary via the build command.
6
+ *
7
+ * Blob names are hashed by bun (e.g. `button-a1b2c3d4.ttl`), losing
8
+ * directory and package association. The loader therefore returns a
9
+ * single SemanticPackage containing ALL embedded TTL content. RDF
10
+ * stores deduplicate triples, so this is safe when combined with
11
+ * higher-precedence loaders that resolve individual packages.
12
+ *
13
+ * @note Impure — reads embedded blobs.
14
+ */
15
+
16
+ import type { PackageRef } from "../../refs/operations/parseRef.js";
17
+ import type {
18
+ GraphContent,
19
+ PackageLoader,
20
+ SemanticPackage,
21
+ } from "../semanticPackage.js";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // State
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** Cached result — embedded content is immutable, no need to re-read. */
28
+ let cached: SemanticPackage | undefined;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Public API
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export default function createBundledLoader(): PackageLoader {
35
+ return {
36
+ name: "bundled",
37
+ async resolve(_ref: PackageRef): Promise<SemanticPackage | undefined> {
38
+ if (cached) return cached;
39
+
40
+ if (typeof globalThis.Bun === "undefined") return undefined;
41
+ // Bun.embeddedFiles blobs have a .name property at runtime
42
+ const blobs = (globalThis.Bun as { embeddedFiles?: readonly Blob[] })
43
+ .embeddedFiles as ReadonlyArray<Blob & { name: string }> | undefined;
44
+ if (!blobs?.length) return undefined;
45
+
46
+ const graphs = await readEmbeddedGraphs(blobs);
47
+ if (graphs.length === 0) return undefined;
48
+
49
+ const version = await readEmbeddedVersion(blobs);
50
+
51
+ cached = {
52
+ name: "(bundled)",
53
+ version,
54
+ source: "bundled",
55
+ graphs,
56
+ skills: [],
57
+ };
58
+
59
+ return cached;
60
+ },
61
+ };
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Graph reading
66
+ // ---------------------------------------------------------------------------
67
+
68
+ async function readEmbeddedGraphs(
69
+ blobs: ReadonlyArray<Blob & { name: string }>,
70
+ ): Promise<GraphContent[]> {
71
+ const graphs: GraphContent[] = [];
72
+
73
+ for (const blob of blobs) {
74
+ if (!blob.name.endsWith(".ttl")) continue;
75
+
76
+ try {
77
+ const content = await blob.text();
78
+ graphs.push({
79
+ path: `(bundled)/${blob.name}`,
80
+ content,
81
+ format: "turtle",
82
+ });
83
+ } catch {}
84
+ }
85
+
86
+ return graphs;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Version reading
91
+ // ---------------------------------------------------------------------------
92
+
93
+ async function readEmbeddedVersion(
94
+ blobs: ReadonlyArray<Blob & { name: string }>,
95
+ ): Promise<string> {
96
+ for (const blob of blobs) {
97
+ if (!blob.name.includes("package") || !blob.name.endsWith(".json"))
98
+ continue;
99
+
100
+ try {
101
+ const text = await blob.text();
102
+ const parsed = JSON.parse(text) as { version?: string };
103
+ if (parsed.version) return parsed.version;
104
+ } catch {}
105
+ }
106
+
107
+ return "0.0.0";
108
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Git cache package loader.
3
+ *
4
+ * Resolves semantic packages from the git ref cache at
5
+ * `~/.cache/pragma/refs/<pkg>/<ref>/`, populated by `pragma update-refs`.
6
+ *
7
+ * Only handles `git` kind refs. File and npm refs are skipped.
8
+ *
9
+ * @note Impure — reads filesystem.
10
+ */
11
+
12
+ import { existsSync } from "node:fs";
13
+ import type { PackageRef } from "../../refs/operations/parseRef.js";
14
+ import { gitCacheDir } from "../../refs/operations/paths.js";
15
+ import type { PackageLoader, SemanticPackage } from "../semanticPackage.js";
16
+ import readPackageDir from "./readPackageDir.js";
17
+
18
+ export default function createGitLoader(): PackageLoader {
19
+ return {
20
+ name: "git",
21
+ resolve(ref: PackageRef): SemanticPackage | undefined {
22
+ if (ref.kind !== "git") return undefined;
23
+
24
+ const dir = gitCacheDir(ref.pkg, ref.ref);
25
+ if (!existsSync(dir)) return undefined;
26
+
27
+ const { version, graphs, skills } = readPackageDir(dir);
28
+ return { name: ref.pkg, version, source: "git", graphs, skills };
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,6 @@
1
+ /** @module Semantic package loaders — local, git, and bundled resolution. */
2
+
3
+ export { default as createBundledLoader } from "./bundledLoader.js";
4
+ export { default as createGitLoader } from "./gitLoader.js";
5
+ export { default as createLocalLoader } from "./localLoader.js";
6
+ export { default as readPackageDir } from "./readPackageDir.js";
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Local package loader.
3
+ *
4
+ * Resolves semantic packages from the local filesystem:
5
+ * - `file://` refs → direct path check
6
+ * - All ref kinds → upward `node_modules` walk from this module's location
7
+ *
8
+ * The node_modules walk avoids `require.resolve('pkg/package.json')` which
9
+ * breaks for packages with strict `exports` fields (e.g. @canonical/anatomy-dsl).
10
+ *
11
+ * @note Impure — reads filesystem.
12
+ */
13
+
14
+ import { existsSync, realpathSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import type { PackageRef } from "../../refs/operations/parseRef.js";
17
+ import type { PackageLoader, SemanticPackage } from "../semanticPackage.js";
18
+ import readPackageDir from "./readPackageDir.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Public API
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export default function createLocalLoader(): PackageLoader {
25
+ return {
26
+ name: "local",
27
+ resolve(ref: PackageRef): SemanticPackage | undefined {
28
+ // file:// refs → direct path check first
29
+ if (ref.kind === "file") {
30
+ if (!existsSync(ref.path)) return undefined;
31
+ const { version, graphs, skills } = readPackageDir(ref.path);
32
+ return { name: ref.pkg, version, source: "local", graphs, skills };
33
+ }
34
+
35
+ // All ref kinds → try node_modules walk
36
+ const dir = findPackageDir(ref.pkg);
37
+ if (!dir) return undefined;
38
+
39
+ const { version, graphs, skills } = readPackageDir(dir);
40
+ return { name: ref.pkg, version, source: "local", graphs, skills };
41
+ },
42
+ };
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // node_modules walk
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /**
50
+ * Find a package directory by walking up from this module's location.
51
+ *
52
+ * Checks each ancestor's `node_modules/<pkg>` directory. Handles both
53
+ * scoped (`@scope/name`) and unscoped packages. Follows symlinks via
54
+ * `realpathSync` so hoisted workspace layouts resolve correctly.
55
+ */
56
+ function findPackageDir(pkg: string): string | undefined {
57
+ const segments = pkg.split("/");
58
+ let current = import.meta.dirname;
59
+
60
+ while (current !== dirname(current)) {
61
+ const candidate = join(current, "node_modules", ...segments);
62
+ if (existsSync(candidate)) {
63
+ try {
64
+ return realpathSync(candidate);
65
+ } catch {
66
+ return candidate;
67
+ }
68
+ }
69
+ current = dirname(current);
70
+ }
71
+
72
+ return undefined;
73
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Read semantic content from a package directory on disk.
3
+ *
4
+ * Shared by LocalLoader and GitLoader. Given a directory path, reads
5
+ * package.json for version, globs TTL files from definitions/ and data/,
6
+ * and scans skills/ for subdirectories.
7
+ *
8
+ * @note Impure — reads filesystem.
9
+ */
10
+
11
+ import {
12
+ existsSync,
13
+ globSync,
14
+ readdirSync,
15
+ readFileSync,
16
+ statSync,
17
+ } from "node:fs";
18
+ import { join } from "node:path";
19
+ import type { GraphContent, SkillEntry } from "../semanticPackage.js";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // TTL discovery convention
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const TTL_DIRS: readonly { dir: string; glob: string }[] = [
26
+ { dir: "definitions", glob: "definitions/**/*.ttl" },
27
+ { dir: "data", glob: "data/**/*.ttl" },
28
+ ];
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Public API
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export interface PackageDirContents {
35
+ readonly version: string;
36
+ readonly graphs: GraphContent[];
37
+ readonly skills: SkillEntry[];
38
+ }
39
+
40
+ /**
41
+ * Read semantic package content from a directory.
42
+ *
43
+ * @param dir - Absolute path to the package root directory.
44
+ * @returns Package version, TTL graph content, and skill entries.
45
+ */
46
+ export default function readPackageDir(dir: string): PackageDirContents {
47
+ return {
48
+ version: readVersion(dir),
49
+ graphs: readGraphs(dir),
50
+ skills: readSkills(dir),
51
+ };
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Helpers
56
+ // ---------------------------------------------------------------------------
57
+
58
+ function readVersion(dir: string): string {
59
+ const pjsonPath = join(dir, "package.json");
60
+ try {
61
+ const raw = readFileSync(pjsonPath, "utf-8");
62
+ const parsed = JSON.parse(raw) as { version?: string };
63
+ return parsed.version ?? "0.0.0";
64
+ } catch {
65
+ return "0.0.0";
66
+ }
67
+ }
68
+
69
+ function readGraphs(dir: string): GraphContent[] {
70
+ const graphs: GraphContent[] = [];
71
+
72
+ for (const entry of TTL_DIRS) {
73
+ if (!existsSync(join(dir, entry.dir))) continue;
74
+
75
+ const files = globSync(entry.glob, { cwd: dir });
76
+ for (const file of files) {
77
+ const absPath = join(dir, file);
78
+ try {
79
+ const content = readFileSync(absPath, "utf-8");
80
+ graphs.push({ path: absPath, content, format: "turtle" });
81
+ } catch {
82
+ // Unreadable file — skip silently
83
+ }
84
+ }
85
+ }
86
+
87
+ return graphs;
88
+ }
89
+
90
+ function readSkills(dir: string): SkillEntry[] {
91
+ const skillsDir = join(dir, "skills");
92
+ if (!existsSync(skillsDir)) return [];
93
+
94
+ const skills: SkillEntry[] = [];
95
+ try {
96
+ const entries = readdirSync(skillsDir);
97
+ for (const entry of entries) {
98
+ const entryPath = join(skillsDir, entry);
99
+ try {
100
+ if (statSync(entryPath).isDirectory()) {
101
+ skills.push({ dir: entryPath, folderName: entry });
102
+ }
103
+ } catch {
104
+ // stat failed — skip
105
+ }
106
+ }
107
+ } catch {
108
+ // readdir failed — no skills
109
+ }
110
+
111
+ return skills;
112
+ }
@@ -11,7 +11,12 @@ export default async function lookupMany<TResult>(
11
11
  queries: readonly string[],
12
12
  lookup: (query: string) => Promise<TResult>,
13
13
  ): Promise<LookupResult<TResult>> {
14
- const errors: Array<{ query: string; code: string; message: string }> = [];
14
+ const errors: Array<{
15
+ query: string;
16
+ code: string;
17
+ message: string;
18
+ suggestions?: readonly string[];
19
+ }> = [];
15
20
  const settled = await Promise.allSettled(
16
21
  queries.map((query) => lookup(query)),
17
22
  );
@@ -35,6 +40,9 @@ export default async function lookupMany<TResult>(
35
40
  query,
36
41
  code: error.code,
37
42
  message: error.message,
43
+ ...(error.suggestions.length > 0 && {
44
+ suggestions: error.suggestions,
45
+ }),
38
46
  });
39
47
  } else {
40
48
  throw error;