@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.
- package/dist/pragma +0 -0
- package/package.json +19 -17
- package/src/config/readConfig.ts +38 -2
- package/src/config/types.ts +12 -1
- package/src/config/writeConfig.ts +12 -0
- package/src/constants.ts +12 -1
- package/src/domains/block/commands/index.ts +2 -1
- package/src/domains/block/commands/sample.ts +55 -0
- package/src/domains/block/formatters/index.ts +2 -1
- package/src/domains/block/formatters/sample.ts +21 -0
- package/src/domains/block/index.ts +3 -3
- package/src/domains/block/mcp/specs.ts +48 -0
- package/src/domains/block/operations/index.ts +2 -1
- package/src/domains/block/operations/lookup.ts +9 -0
- package/src/domains/block/operations/sample.ts +41 -0
- package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
- package/src/domains/block/orchestration/resolveBlockLookup.ts +10 -3
- package/src/domains/config/commands/framework.test.ts +88 -0
- package/src/domains/config/commands/framework.ts +85 -0
- package/src/domains/config/commands/index.ts +2 -0
- package/src/domains/config/commands/trace.test.ts +148 -0
- package/src/domains/config/commands/trace.ts +80 -0
- package/src/domains/config/formatters/framework.ts +34 -0
- package/src/domains/config/formatters/index.ts +2 -0
- package/src/domains/config/formatters/trace.ts +28 -0
- package/src/domains/config/index.ts +15 -2
- package/src/domains/config/operations/index.ts +1 -0
- package/src/domains/config/operations/validateFramework.ts +24 -0
- package/src/domains/config/tasks/index.ts +2 -0
- package/src/domains/config/tasks/setChannel.ts +7 -1
- package/src/domains/config/tasks/setFramework.ts +40 -0
- package/src/domains/config/tasks/setTier.ts +7 -1
- package/src/domains/config/tasks/setTrace.ts +38 -0
- package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +35 -41
- package/src/domains/doctor/operations/checks.test.ts +4 -1
- package/src/domains/doctor/operations/runChecks.test.ts +72 -0
- package/src/domains/doctor/operations/runChecks.ts +48 -12
- package/src/domains/graphql/commands/build.test.ts +194 -0
- package/src/domains/graphql/commands/build.ts +125 -0
- package/src/domains/graphql/commands/check.test.ts +139 -0
- package/src/domains/graphql/commands/check.ts +87 -0
- package/src/domains/graphql/commands/index.ts +4 -0
- package/src/domains/graphql/formatters/index.ts +3 -0
- package/src/domains/graphql/formatters/report.ts +78 -0
- package/src/domains/graphql/helpers/index.ts +4 -0
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
- package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
- package/src/domains/graphql/index.ts +21 -0
- package/src/domains/graphql/operations/compileSchema.test.ts +119 -0
- package/src/domains/graphql/operations/compileSchema.ts +109 -0
- package/src/domains/graphql/operations/index.ts +7 -0
- package/src/domains/graphql/types.ts +29 -0
- package/src/domains/info/operations/collectInfo.test.ts +2 -2
- package/src/domains/info/operations/collectInfo.ts +4 -4
- package/src/domains/llm/capabilities.test.ts +91 -0
- package/src/domains/llm/commands/capabilities.ts +28 -0
- package/src/domains/llm/commands/index.ts +1 -0
- package/src/domains/llm/data/commandReference.ts +90 -18
- package/src/domains/llm/data/decisionTrees.ts +15 -13
- package/src/domains/llm/data/index.ts +2 -0
- package/src/domains/llm/data/toolCatalog.ts +184 -0
- package/src/domains/llm/formatters/capabilities.ts +50 -0
- package/src/domains/llm/formatters/index.ts +1 -0
- package/src/domains/llm/index.ts +3 -2
- package/src/domains/llm/mcp/index.ts +1 -1
- package/src/domains/llm/mcp/specs.ts +59 -52
- package/src/domains/llm/types.ts +40 -0
- package/src/domains/modifier/commands/index.ts +2 -1
- package/src/domains/modifier/commands/sample.ts +53 -0
- package/src/domains/modifier/formatters/index.ts +2 -1
- package/src/domains/modifier/formatters/sample.ts +15 -0
- package/src/domains/modifier/index.ts +7 -3
- package/src/domains/modifier/mcp/specs.ts +47 -0
- package/src/domains/modifier/operations/index.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +9 -0
- package/src/domains/modifier/operations/sample.ts +32 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +14 -2
- package/src/domains/refs/operations/index.ts +1 -1
- package/src/domains/refs/operations/paths.ts +29 -1
- package/src/domains/refs/operations/updateRefs.test.ts +6 -3
- package/src/domains/refs/operations/updateRefs.ts +3 -2
- package/src/domains/shared/bootStore.ts +88 -49
- package/src/domains/shared/contracts.ts +1 -0
- package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
- package/src/domains/shared/loaders/bundledLoader.ts +108 -0
- package/src/domains/shared/loaders/gitLoader.ts +31 -0
- package/src/domains/shared/loaders/index.ts +6 -0
- package/src/domains/shared/loaders/localLoader.ts +73 -0
- package/src/domains/shared/loaders/readPackageDir.ts +112 -0
- package/src/domains/shared/lookupMany.ts +9 -1
- package/src/domains/shared/packages.ts +21 -120
- package/src/domains/shared/pickRandom.ts +22 -0
- package/src/domains/shared/runtime.ts +6 -3
- package/src/domains/shared/sampleFormatters.ts +47 -0
- package/src/domains/shared/semanticPackage.ts +96 -0
- package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
- package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
- package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
- package/src/domains/shared/suggestions/expandGlob.ts +51 -0
- package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
- package/src/domains/shared/suggestions/index.ts +7 -0
- package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
- package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
- package/src/domains/shared/suggestions/suggestNames.ts +97 -0
- package/src/domains/shared/types/index.ts +1 -0
- package/src/domains/shared/types/runtime.ts +4 -1
- package/src/domains/shared/types/sample.ts +17 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +49 -16
- package/src/domains/standard/commands/index.ts +2 -1
- package/src/domains/standard/commands/sample.ts +57 -0
- package/src/domains/standard/formatters/index.ts +2 -1
- package/src/domains/standard/formatters/sample.ts +15 -0
- package/src/domains/standard/index.ts +8 -1
- package/src/domains/standard/mcp/specs.ts +48 -1
- package/src/domains/standard/operations/index.ts +2 -1
- package/src/domains/standard/operations/lookup.ts +9 -0
- package/src/domains/standard/operations/sample.ts +35 -0
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +14 -2
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
- package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
- package/src/domains/token/commands/index.ts +2 -1
- package/src/domains/token/commands/sample.ts +53 -0
- package/src/domains/token/formatters/index.ts +2 -1
- package/src/domains/token/formatters/sample.ts +16 -0
- package/src/domains/token/index.ts +8 -1
- package/src/domains/token/mcp/specs.ts +47 -1
- package/src/domains/token/operations/index.ts +2 -1
- package/src/domains/token/operations/lookup.ts +9 -0
- package/src/domains/token/operations/sample.ts +32 -0
- package/src/domains/token/orchestration/resolveTokenLookup.ts +14 -2
- package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
- package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
- package/src/domains/trace/commands/clear.ts +71 -0
- package/src/domains/trace/commands/index.ts +5 -0
- package/src/domains/trace/commands/sessions.ts +32 -0
- package/src/domains/trace/commands/show.ts +145 -0
- package/src/domains/trace/formatters/index.ts +9 -0
- package/src/domains/trace/formatters/sessions.ts +80 -0
- package/src/domains/trace/formatters/show.ts +122 -0
- package/src/domains/trace/index.ts +17 -0
- package/src/domains/trace/operations/index.ts +8 -0
- package/src/domains/trace/operations/listSessions.test.ts +55 -0
- package/src/domains/trace/operations/listSessions.ts +52 -0
- package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
- package/src/domains/trace/operations/readTraceLog.ts +104 -0
- package/src/domains/trace/tracePlugin.test.ts +171 -0
- package/src/domains/trace/tracePlugin.ts +159 -0
- package/src/domains/trace/types.ts +65 -0
- package/src/error/PragmaError.ts +15 -2
- package/src/error/types.ts +15 -1
- package/src/mcp/error/serializeErrorPayload.ts +7 -0
- package/src/mcp/error/types.ts +8 -0
- package/src/mcp/registerResources.test.ts +38 -16
- package/src/mcp/registerResources.ts +57 -8
- package/src/mcp/tools/registerTools.test.ts +22 -7
- package/src/mcp/utils/wrapTool.test.ts +1 -0
- package/src/pipeline/collectCommands.ts +7 -1
- package/src/pipeline/renderError.ts +15 -0
- package/src/pipeline/resolveCommandKind.ts +8 -1
- package/src/pipeline/runCli.ts +12 -1
- package/src/testing/createTestMcpClient.ts +1 -0
- package/src/testing/graphqlFixtures.ts +72 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/integration/mcp-surface.test.ts +11 -5
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Store } from "@canonical/ke";
|
|
2
|
+
import expandGlob, { isGlobPattern } from "./expandGlob.js";
|
|
3
|
+
import type { Domain } from "./listDomainNames.js";
|
|
4
|
+
import listDomainNames from "./listDomainNames.js";
|
|
5
|
+
|
|
6
|
+
interface ExpandedQueries {
|
|
7
|
+
/** Concrete names to pass to lookupMany. */
|
|
8
|
+
readonly names: string[];
|
|
9
|
+
/** Errors for glob patterns that matched nothing. */
|
|
10
|
+
readonly globErrors: readonly {
|
|
11
|
+
query: string;
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Partition lookup queries into concrete names and expanded globs.
|
|
19
|
+
*
|
|
20
|
+
* Literal names pass through unchanged. Glob patterns are expanded against
|
|
21
|
+
* the domain's full name list. Globs that match nothing produce an
|
|
22
|
+
* EMPTY_RESULTS error entry.
|
|
23
|
+
*/
|
|
24
|
+
export default async function expandLookupQueries(
|
|
25
|
+
queries: readonly string[],
|
|
26
|
+
store: Store,
|
|
27
|
+
domain: Domain,
|
|
28
|
+
): Promise<ExpandedQueries> {
|
|
29
|
+
const hasGlobs = queries.some(isGlobPattern);
|
|
30
|
+
if (!hasGlobs) {
|
|
31
|
+
return { names: [...queries], globErrors: [] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const allNames = await listDomainNames(store, domain);
|
|
35
|
+
const names: string[] = [];
|
|
36
|
+
const globErrors: { query: string; code: string; message: string }[] = [];
|
|
37
|
+
|
|
38
|
+
for (const query of queries) {
|
|
39
|
+
if (!isGlobPattern(query)) {
|
|
40
|
+
names.push(query);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const expanded = expandGlob(query, allNames);
|
|
45
|
+
if (expanded.length === 0) {
|
|
46
|
+
globErrors.push({
|
|
47
|
+
query,
|
|
48
|
+
code: "EMPTY_RESULTS",
|
|
49
|
+
message: `No ${domain}s matching "${query}".`,
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
names.push(...expanded);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { names, globErrors };
|
|
57
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { CrossDomainHint } from "./detectCrossDomain.js";
|
|
2
|
+
export { default as detectCrossDomain } from "./detectCrossDomain.js";
|
|
3
|
+
export { default as expandGlob, isGlobPattern } from "./expandGlob.js";
|
|
4
|
+
export { default as expandLookupQueries } from "./expandLookupQueries.js";
|
|
5
|
+
export type { Domain } from "./listDomainNames.js";
|
|
6
|
+
export { default as listDomainNames } from "./listDomainNames.js";
|
|
7
|
+
export { default as suggestNames } from "./suggestNames.js";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight name queries for each domain.
|
|
3
|
+
*
|
|
4
|
+
* Used by suggestNames to fetch candidate lists when a lookup fails.
|
|
5
|
+
* Each query returns only names (no detail) for minimal overhead.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SPARQL, Store } from "@canonical/ke";
|
|
9
|
+
import { buildQuery } from "../buildQuery.js";
|
|
10
|
+
import { P } from "../prefixes.js";
|
|
11
|
+
|
|
12
|
+
type Domain = "block" | "token" | "modifier" | "standard" | "ontology";
|
|
13
|
+
|
|
14
|
+
const QUERIES: Record<Domain, SPARQL<string>> = {
|
|
15
|
+
block: buildQuery(`
|
|
16
|
+
SELECT DISTINCT ?name WHERE {
|
|
17
|
+
VALUES ?type { ${P.ds}Component ${P.ds}Pattern ${P.ds}Layout ${P.ds}Subcomponent }
|
|
18
|
+
?s a ?type ; ${P.ds}name ?name .
|
|
19
|
+
}
|
|
20
|
+
`),
|
|
21
|
+
token: buildQuery(`
|
|
22
|
+
SELECT DISTINCT ?name WHERE {
|
|
23
|
+
?s a ${P.ds}Token ; ${P.ds}tokenId ?name .
|
|
24
|
+
}
|
|
25
|
+
`),
|
|
26
|
+
modifier: buildQuery(`
|
|
27
|
+
SELECT DISTINCT ?name WHERE {
|
|
28
|
+
?s a ${P.ds}ModifierFamily ; ${P.ds}name ?name .
|
|
29
|
+
}
|
|
30
|
+
`),
|
|
31
|
+
standard: buildQuery(`
|
|
32
|
+
SELECT DISTINCT ?name WHERE {
|
|
33
|
+
?s a ${P.cs}CodeStandard ; ${P.cs}name ?name .
|
|
34
|
+
}
|
|
35
|
+
`),
|
|
36
|
+
ontology: buildQuery(`
|
|
37
|
+
SELECT DISTINCT ?name WHERE {
|
|
38
|
+
?s a owl:Ontology .
|
|
39
|
+
BIND(STR(?s) AS ?name)
|
|
40
|
+
}
|
|
41
|
+
`),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fetch all entity names for a domain from the store.
|
|
46
|
+
*
|
|
47
|
+
* @note - This function is impure — it queries the ke store.
|
|
48
|
+
*/
|
|
49
|
+
export default async function listDomainNames(
|
|
50
|
+
store: Store,
|
|
51
|
+
domain: Domain,
|
|
52
|
+
): Promise<string[]> {
|
|
53
|
+
const query = QUERIES[domain];
|
|
54
|
+
const result = await store.query(query);
|
|
55
|
+
if (result.type !== "select") return [];
|
|
56
|
+
return result.bindings
|
|
57
|
+
.map((b) => b.name)
|
|
58
|
+
.filter((n): n is string => typeof n === "string" && n !== "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type { Domain };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import suggestNames from "./suggestNames.js";
|
|
3
|
+
|
|
4
|
+
describe("suggestNames", () => {
|
|
5
|
+
it("T1-1: suggests edit-distance match for single-char typo", () => {
|
|
6
|
+
expect(suggestNames("Buton", ["Button", "Card", "Input"])).toEqual([
|
|
7
|
+
"Button",
|
|
8
|
+
]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("T1-2: suggests dot-separated names with typo in last segment", () => {
|
|
12
|
+
const candidates = [
|
|
13
|
+
"react.component.props",
|
|
14
|
+
"react.component.props.wrapper",
|
|
15
|
+
"react.component.props.html_rendering",
|
|
16
|
+
"react.component.naming",
|
|
17
|
+
"css.selectors.namespace",
|
|
18
|
+
];
|
|
19
|
+
const result = suggestNames("react.component.prosp", candidates);
|
|
20
|
+
expect(result.at(0)).toBe("react.component.props");
|
|
21
|
+
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("T1-3: matches case-insensitively, preserves original casing", () => {
|
|
25
|
+
expect(
|
|
26
|
+
suggestNames("buton-background", ["Button-Background", "color-primary"]),
|
|
27
|
+
).toEqual(["Button-Background"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("T1-4: returns prefix matches ranked first", () => {
|
|
31
|
+
const result = suggestNames("Accordion", [
|
|
32
|
+
"AccordionItem",
|
|
33
|
+
"AccordionGroup",
|
|
34
|
+
"Button",
|
|
35
|
+
]);
|
|
36
|
+
expect(result).toHaveLength(2);
|
|
37
|
+
expect(result).toContain("AccordionItem");
|
|
38
|
+
expect(result).toContain("AccordionGroup");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("T1-5: returns empty for completely unrelated input", () => {
|
|
42
|
+
expect(suggestNames("zzzzz", ["Button", "Card", "Input"])).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("T1-6: returns empty for empty query", () => {
|
|
46
|
+
expect(suggestNames("", ["Button"])).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("T1-7: excludes exact matches", () => {
|
|
50
|
+
expect(suggestNames("Button", ["Button", "Card"])).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("T1-8: ranks multiple candidates by distance", () => {
|
|
54
|
+
const result = suggestNames("Buttn", ["Button", "Butter", "Bitten"]);
|
|
55
|
+
expect(result).toContain("Button");
|
|
56
|
+
expect(result).toContain("Butter");
|
|
57
|
+
expect(result.indexOf("Button")).toBeLessThan(result.indexOf("Butter"));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("respects maxResults option", () => {
|
|
61
|
+
const candidates = Array.from({ length: 20 }, (_, i) => `Item${i}`);
|
|
62
|
+
const result = suggestNames("Item", candidates, { maxResults: 3 });
|
|
63
|
+
expect(result.length).toBeLessThanOrEqual(3);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("respects threshold option", () => {
|
|
67
|
+
const result = suggestNames("Buton", ["Button"], { threshold: 0.05 });
|
|
68
|
+
expect(result).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rank candidates by similarity to a failed lookup query.
|
|
3
|
+
*
|
|
4
|
+
* Returns up to {@link maxResults} names sorted by relevance:
|
|
5
|
+
* 1. Prefix matches (query is a case-insensitive prefix of candidate)
|
|
6
|
+
* 2. Edit-distance matches (Damerau-Levenshtein, normalized ≤ threshold)
|
|
7
|
+
*
|
|
8
|
+
* Exact matches are excluded — if the query exactly matches a candidate,
|
|
9
|
+
* the caller should have found it and never reached this function.
|
|
10
|
+
*
|
|
11
|
+
* @param query - The user-supplied name that failed lookup.
|
|
12
|
+
* @param candidates - All known names in the domain.
|
|
13
|
+
* @param opts.maxResults - Max suggestions to return (default: 5).
|
|
14
|
+
* @param opts.threshold - Max normalized edit distance (default: 0.4).
|
|
15
|
+
* @returns Ranked suggestions with original casing preserved.
|
|
16
|
+
*/
|
|
17
|
+
export default function suggestNames(
|
|
18
|
+
query: string,
|
|
19
|
+
candidates: readonly string[],
|
|
20
|
+
opts?: { maxResults?: number; threshold?: number },
|
|
21
|
+
): string[] {
|
|
22
|
+
if (query === "") return [];
|
|
23
|
+
|
|
24
|
+
const maxResults = opts?.maxResults ?? 5;
|
|
25
|
+
const threshold = opts?.threshold ?? 0.4;
|
|
26
|
+
const queryLower = query.toLowerCase();
|
|
27
|
+
|
|
28
|
+
type Scored = { name: string; score: number };
|
|
29
|
+
const scored: Scored[] = [];
|
|
30
|
+
|
|
31
|
+
for (const candidate of candidates) {
|
|
32
|
+
const candidateLower = candidate.toLowerCase();
|
|
33
|
+
|
|
34
|
+
if (candidateLower === queryLower) continue;
|
|
35
|
+
|
|
36
|
+
if (candidateLower.startsWith(queryLower)) {
|
|
37
|
+
scored.push({ name: candidate, score: 0 });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const distance = damerauLevenshtein(queryLower, candidateLower);
|
|
42
|
+
const maxLen = Math.max(queryLower.length, candidateLower.length);
|
|
43
|
+
const normalized = maxLen === 0 ? 0 : distance / maxLen;
|
|
44
|
+
|
|
45
|
+
if (normalized <= threshold) {
|
|
46
|
+
scored.push({ name: candidate, score: normalized });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
scored.sort((a, b) => a.score - b.score);
|
|
51
|
+
return scored.slice(0, maxResults).map((s) => s.name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Damerau-Levenshtein distance between two strings.
|
|
56
|
+
*
|
|
57
|
+
* Counts insertions, deletions, substitutions, and adjacent transpositions.
|
|
58
|
+
*/
|
|
59
|
+
function damerauLevenshtein(a: string, b: string): number {
|
|
60
|
+
const lenA = a.length;
|
|
61
|
+
const lenB = b.length;
|
|
62
|
+
|
|
63
|
+
if (lenA === 0) return lenB;
|
|
64
|
+
if (lenB === 0) return lenA;
|
|
65
|
+
|
|
66
|
+
const matrix: number[][] = Array.from({ length: lenA + 1 }, () =>
|
|
67
|
+
Array.from<number>({ length: lenB + 1 }).fill(0),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i <= lenA; i++) {
|
|
71
|
+
matrix[i]![0] = i;
|
|
72
|
+
}
|
|
73
|
+
for (let j = 0; j <= lenB; j++) {
|
|
74
|
+
matrix[0]![j] = j;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (let i = 1; i <= lenA; i++) {
|
|
78
|
+
for (let j = 1; j <= lenB; j++) {
|
|
79
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
80
|
+
|
|
81
|
+
matrix[i]![j] = Math.min(
|
|
82
|
+
matrix[i - 1]![j]! + 1, // deletion
|
|
83
|
+
matrix[i]![j - 1]! + 1, // insertion
|
|
84
|
+
matrix[i - 1]![j - 1]! + cost, // substitution
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
88
|
+
matrix[i]![j] = Math.min(
|
|
89
|
+
matrix[i]![j]!,
|
|
90
|
+
matrix[i - 2]![j - 2]! + cost, // transposition
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return matrix[lenA]![lenB]!;
|
|
97
|
+
}
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Store } from "@canonical/ke";
|
|
4
4
|
import type { PragmaConfig } from "#config";
|
|
5
|
+
import type { SemanticPackage } from "../semanticPackage.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Boot-time state shared by CLI, MCP, and completions server.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Five members:
|
|
10
11
|
* - `store` — the ke triple store, loaded and ready for SPARQL queries.
|
|
11
12
|
* - `config` — resolved pragma config (tier, channel).
|
|
12
13
|
* - `cwd` — working directory used for config and source resolution.
|
|
14
|
+
* - `packages` — resolved semantic packages (for diagnostics and skills).
|
|
13
15
|
* - `dispose()` — tears down the store and frees WASM memory. Must be
|
|
14
16
|
* called exactly once; calling operations after dispose is undefined.
|
|
15
17
|
*/
|
|
@@ -17,5 +19,6 @@ export interface PragmaRuntime {
|
|
|
17
19
|
readonly store: Store;
|
|
18
20
|
readonly config: PragmaConfig;
|
|
19
21
|
readonly cwd: string;
|
|
22
|
+
readonly packages: readonly SemanticPackage[];
|
|
20
23
|
dispose(): void;
|
|
21
24
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for sample operations across domains.
|
|
3
|
+
*
|
|
4
|
+
* Sample returns 1–5 complete instances as exemplars so agents
|
|
5
|
+
* can learn actual data shapes before writing queries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Raw result from a sample operation — instances plus total count. */
|
|
9
|
+
export interface SampleResult<T> {
|
|
10
|
+
readonly samples: readonly T[];
|
|
11
|
+
readonly totalCount: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Full sample output including agent guidance. */
|
|
15
|
+
export interface SampleOutput<T> extends SampleResult<T> {
|
|
16
|
+
readonly nextSteps: readonly string[];
|
|
17
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { parsePackageEntry } from "../../refs/operations/parseRef.js";
|
|
2
|
+
import {
|
|
3
|
+
createGitLoader,
|
|
4
|
+
createLocalLoader,
|
|
5
|
+
} from "../../shared/loaders/index.js";
|
|
6
|
+
import { DEFAULT_PACKAGES } from "../../shared/packages.js";
|
|
7
|
+
import type { SemanticPackage } from "../../shared/semanticPackage.js";
|
|
8
|
+
import { resolveSemanticPackages } from "../../shared/semanticPackage.js";
|
|
5
9
|
|
|
6
10
|
/** A resolved skill source directory from an installed package. */
|
|
7
11
|
export interface SkillSource {
|
|
@@ -12,27 +16,56 @@ export interface SkillSource {
|
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
|
-
* Resolve skill source directories from
|
|
19
|
+
* Resolve skill source directories from semantic packages.
|
|
16
20
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
21
|
+
* When `packages` is provided (from PragmaRuntime), extracts skill entries
|
|
22
|
+
* directly. Otherwise, resolves packages using local and git loaders
|
|
23
|
+
* (bundled loader is excluded since it has no filesystem skills).
|
|
19
24
|
*
|
|
20
|
-
* @param
|
|
25
|
+
* @param packages - Pre-resolved semantic packages (from boot).
|
|
21
26
|
* @returns Array of resolved skill sources with absolute paths.
|
|
22
|
-
* @note Impure
|
|
27
|
+
* @note Impure when packages is omitted (reads filesystem).
|
|
23
28
|
*/
|
|
24
29
|
export default function resolveSkillSources(
|
|
25
|
-
|
|
30
|
+
packages?: readonly SemanticPackage[],
|
|
31
|
+
): SkillSource[] {
|
|
32
|
+
if (packages) {
|
|
33
|
+
return extractSkillSources(packages);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback: resolve packages ourselves (skills need filesystem paths,
|
|
37
|
+
// so we skip the bundled loader here)
|
|
38
|
+
return resolveSkillSourcesFromRefs();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractSkillSources(
|
|
42
|
+
packages: readonly SemanticPackage[],
|
|
26
43
|
): SkillSource[] {
|
|
27
44
|
const sources: SkillSource[] = [];
|
|
28
|
-
const
|
|
45
|
+
for (const pkg of packages) {
|
|
46
|
+
for (const skill of pkg.skills) {
|
|
47
|
+
sources.push({ dir: skill.dir, packageName: pkg.name });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return sources;
|
|
51
|
+
}
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
function resolveSkillSourcesFromRefs(): SkillSource[] {
|
|
54
|
+
const refs = DEFAULT_PACKAGES.map(parsePackageEntry);
|
|
55
|
+
const loaders = [createLocalLoader(), createGitLoader()];
|
|
56
|
+
|
|
57
|
+
// resolveSemanticPackages is async but skills resolution is sync.
|
|
58
|
+
// Use local+git loaders which are both sync.
|
|
59
|
+
const packages: SemanticPackage[] = [];
|
|
60
|
+
for (const ref of refs) {
|
|
61
|
+
for (const loader of loaders) {
|
|
62
|
+
const resolved = loader.resolve(ref);
|
|
63
|
+
if (resolved && !(resolved instanceof Promise)) {
|
|
64
|
+
packages.push(resolved);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
34
67
|
}
|
|
35
68
|
}
|
|
36
69
|
|
|
37
|
-
return
|
|
70
|
+
return extractSkillSources(packages);
|
|
38
71
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/** @module Re-exports standard command builders (`categories`, `
|
|
1
|
+
/** @module Re-exports standard command builders (`categories`, `list`, `lookup`, `sample`). */
|
|
2
2
|
|
|
3
3
|
export { default as categoriesCommand } from "./categories.js";
|
|
4
4
|
export { default as listCommand } from "./list.js";
|
|
5
5
|
export { default as lookupCommand } from "./lookup.js";
|
|
6
|
+
export { default as sampleCommand } from "./sample.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires the `pragma standard sample` CLI command.
|
|
3
|
+
*
|
|
4
|
+
* Returns 1–5 complete standard instances as exemplars so agents
|
|
5
|
+
* can learn the data shape before writing queries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type CommandDefinition,
|
|
10
|
+
createOutputResult,
|
|
11
|
+
} from "@canonical/cli-core";
|
|
12
|
+
import type { PragmaContext } from "../../shared/context.js";
|
|
13
|
+
import { selectFormatter } from "../../shared/formatters.js";
|
|
14
|
+
import type {
|
|
15
|
+
SampleOutput,
|
|
16
|
+
StandardDetailed,
|
|
17
|
+
} from "../../shared/types/index.js";
|
|
18
|
+
import { sampleFormatters } from "../formatters/index.js";
|
|
19
|
+
import { sampleStandards } from "../operations/index.js";
|
|
20
|
+
|
|
21
|
+
const NEXT_STEPS = [
|
|
22
|
+
"Use standard_lookup to inspect specific standards by name.",
|
|
23
|
+
"Use standard_list with --category to filter by category.",
|
|
24
|
+
"Use standard_categories to see all available categories.",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export default function sampleCommand(ctx: PragmaContext): CommandDefinition {
|
|
28
|
+
return {
|
|
29
|
+
path: ["standard", "sample"],
|
|
30
|
+
description:
|
|
31
|
+
"Return randomly selected complete standard instances as exemplars for shape discovery",
|
|
32
|
+
parameters: [
|
|
33
|
+
{
|
|
34
|
+
name: "count",
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Number of samples to return (1–5, default 2)",
|
|
37
|
+
required: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
meta: {
|
|
41
|
+
examples: ["pragma standard sample", "pragma standard sample --count 3"],
|
|
42
|
+
},
|
|
43
|
+
async execute(params) {
|
|
44
|
+
const count = Number(params.count ?? 2);
|
|
45
|
+
const result = await sampleStandards(ctx.store, count);
|
|
46
|
+
|
|
47
|
+
const output: SampleOutput<StandardDetailed> = {
|
|
48
|
+
...result,
|
|
49
|
+
nextSteps: NEXT_STEPS,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return createOutputResult(output, {
|
|
53
|
+
plain: selectFormatter(ctx, sampleFormatters),
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @module Re-exports standard formatters (`categoriesFormatters`, `
|
|
1
|
+
/** @module Re-exports standard formatters (`categoriesFormatters`, `listFormatters`, `lookupFormatters`, `sampleFormatters`). */
|
|
2
2
|
|
|
3
3
|
export { default as categoriesFormatters } from "./categories.js";
|
|
4
4
|
export { default as listFormatters } from "./list.js";
|
|
@@ -6,4 +6,5 @@ export {
|
|
|
6
6
|
createInkLookupOptions,
|
|
7
7
|
default as lookupFormatters,
|
|
8
8
|
} from "./lookup.js";
|
|
9
|
+
export { default as sampleFormatters } from "./sample.js";
|
|
9
10
|
export type { StandardListOutput, StandardLookupInput } from "./types.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample formatter for the standard domain.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to the shared factory, rendering each item via the
|
|
5
|
+
* existing standard lookup formatter.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import createSampleFormatters from "../../shared/sampleFormatters.js";
|
|
9
|
+
import type { StandardDetailed } from "../../shared/types/index.js";
|
|
10
|
+
import lookupFormatters from "./lookup.js";
|
|
11
|
+
|
|
12
|
+
export default createSampleFormatters<StandardDetailed>(
|
|
13
|
+
"standards",
|
|
14
|
+
(standard, mode) => lookupFormatters[mode]({ standard, detailed: true }),
|
|
15
|
+
);
|
|
@@ -12,10 +12,16 @@ import {
|
|
|
12
12
|
categoriesCommand,
|
|
13
13
|
listCommand,
|
|
14
14
|
lookupCommand,
|
|
15
|
+
sampleCommand,
|
|
15
16
|
} from "./commands/index.js";
|
|
16
17
|
|
|
17
18
|
export function commands(ctx: PragmaContext): CommandDefinition[] {
|
|
18
|
-
return [
|
|
19
|
+
return [
|
|
20
|
+
listCommand(ctx),
|
|
21
|
+
lookupCommand(ctx),
|
|
22
|
+
categoriesCommand(ctx),
|
|
23
|
+
sampleCommand(ctx),
|
|
24
|
+
];
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export { specs as mcpSpecs } from "./mcp/index.js";
|
|
@@ -23,6 +29,7 @@ export {
|
|
|
23
29
|
listCategories,
|
|
24
30
|
listStandards,
|
|
25
31
|
lookupStandard,
|
|
32
|
+
sampleStandards,
|
|
26
33
|
} from "./operations/index.js";
|
|
27
34
|
export {
|
|
28
35
|
buildStandardFilters,
|
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
categoriesFormatters as standardCatFmt,
|
|
12
12
|
listFormatters as standardListFmt,
|
|
13
13
|
lookupFormatters as standardLookupFmt,
|
|
14
|
+
sampleFormatters as standardSampleFmt,
|
|
14
15
|
} from "../formatters/index.js";
|
|
15
|
-
import { listCategories } from "../operations/index.js";
|
|
16
|
+
import { listCategories, sampleStandards } from "../operations/index.js";
|
|
16
17
|
import {
|
|
17
18
|
resolveStandardList,
|
|
18
19
|
resolveStandardLookup,
|
|
@@ -192,6 +193,52 @@ const specs: readonly ToolSpec[] = [
|
|
|
192
193
|
return { data: result, meta: { count: result.length } };
|
|
193
194
|
},
|
|
194
195
|
},
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
name: "standard_sample",
|
|
199
|
+
description:
|
|
200
|
+
"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.",
|
|
201
|
+
params: {
|
|
202
|
+
count: {
|
|
203
|
+
type: "string",
|
|
204
|
+
description: "Number of samples (1–5, default 2)",
|
|
205
|
+
optional: true,
|
|
206
|
+
},
|
|
207
|
+
condensed: {
|
|
208
|
+
type: "boolean",
|
|
209
|
+
description: "Token-optimized output",
|
|
210
|
+
optional: true,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
readOnly: true,
|
|
214
|
+
async execute(rt, { count, condensed }) {
|
|
215
|
+
const n = Number(count ?? 2);
|
|
216
|
+
const result = await sampleStandards(rt.store, n);
|
|
217
|
+
const nextSteps = [
|
|
218
|
+
`These are ${result.samples.length} of ${result.totalCount} total standards.`,
|
|
219
|
+
"Use standard_lookup to inspect specific standards by name.",
|
|
220
|
+
"Use standard_list with category param to filter by category.",
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
if (condensed) {
|
|
224
|
+
const text = standardSampleFmt.llm({ ...result, nextSteps });
|
|
225
|
+
return {
|
|
226
|
+
condensed: true,
|
|
227
|
+
text,
|
|
228
|
+
tokens: `~${Math.ceil(text.length / 4)}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
data: {
|
|
234
|
+
samples: result.samples,
|
|
235
|
+
totalCount: result.totalCount,
|
|
236
|
+
nextSteps,
|
|
237
|
+
},
|
|
238
|
+
meta: { count: result.samples.length },
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
},
|
|
195
242
|
];
|
|
196
243
|
|
|
197
244
|
export default specs;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
/** @module Re-exports standard operations (`listCategories`, `
|
|
1
|
+
/** @module Re-exports standard operations (`listCategories`, `listStandards`, `lookupStandard`, `sampleStandards`). */
|
|
2
2
|
|
|
3
3
|
export { default as listCategories } from "./categories.js";
|
|
4
4
|
export { default as listStandards } from "./list.js";
|
|
5
5
|
export { default as lookupStandard } from "./lookup.js";
|
|
6
|
+
export { default as sampleStandards } from "./sample.js";
|
|
@@ -18,6 +18,11 @@ import resolveUri from "../../graph/helpers/resolveUri.js";
|
|
|
18
18
|
import { buildQuery } from "../../shared/buildQuery.js";
|
|
19
19
|
import compactUri from "../../shared/compactUri.js";
|
|
20
20
|
import { P } from "../../shared/prefixes.js";
|
|
21
|
+
import {
|
|
22
|
+
detectCrossDomain,
|
|
23
|
+
listDomainNames,
|
|
24
|
+
suggestNames,
|
|
25
|
+
} from "../../shared/suggestions/index.js";
|
|
21
26
|
import type { CodeBlock, StandardDetailed } from "../../shared/types/index.js";
|
|
22
27
|
|
|
23
28
|
export default async function lookupStandard(
|
|
@@ -52,12 +57,16 @@ export default async function lookupStandard(
|
|
|
52
57
|
);
|
|
53
58
|
|
|
54
59
|
if (baseResult.type !== "select" || baseResult.bindings.length === 0) {
|
|
60
|
+
const candidates = await listDomainNames(store, "standard");
|
|
61
|
+
const crossDomain = await detectCrossDomain(nameOrUri, "standard", store);
|
|
55
62
|
throw PragmaError.notFound("standard", nameOrUri, {
|
|
63
|
+
suggestions: suggestNames(nameOrUri, candidates),
|
|
56
64
|
recovery: {
|
|
57
65
|
message: "List available standards.",
|
|
58
66
|
cli: "pragma standard list",
|
|
59
67
|
mcp: { tool: "standard_list" },
|
|
60
68
|
},
|
|
69
|
+
crossDomain: crossDomain ?? undefined,
|
|
61
70
|
});
|
|
62
71
|
}
|
|
63
72
|
|