@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.
- package/README.md +64 -35
- package/dist/pragma +0 -0
- package/package.json +15 -13
- package/src/completions/constants.ts +7 -0
- package/src/completions/handleQuery.test.ts +32 -0
- package/src/completions/handleQuery.ts +20 -1
- package/src/completions/queryCompletions.test.ts +5 -0
- package/src/completions/queryCompletions.ts +17 -3
- package/src/completions/querySocket.test.ts +31 -0
- package/src/completions/querySocket.ts +36 -9
- package/src/config/parseConfigValues.ts +2 -0
- package/src/config/readConfigLayers.test.ts +2 -0
- package/src/config/readConfigLayers.ts +3 -0
- package/src/config/types.ts +7 -0
- package/src/constants.ts +3 -1
- package/src/domains/block/commands/sample.ts +2 -1
- package/src/domains/block/formatters/list.test.ts +3 -3
- package/src/domains/block/formatters/lookup.test.ts +5 -5
- package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
- package/src/domains/block/stories.ts +9 -3
- package/src/domains/config/commands/channel.test.ts +16 -0
- package/src/domains/config/commands/channel.ts +14 -1
- package/src/domains/config/commands/framework.test.ts +14 -0
- package/src/domains/config/commands/framework.ts +13 -1
- package/src/domains/config/commands/tier.test.ts +7 -0
- package/src/domains/config/commands/tier.ts +14 -1
- package/src/domains/config/operations/validateTier.test.ts +9 -0
- package/src/domains/config/operations/validateTier.ts +10 -2
- package/src/domains/create/generatorPack/bundled.ts +49 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
- package/src/domains/create/index.ts +13 -7
- package/src/domains/create/mcp/specs.ts +6 -161
- package/src/domains/create/renderGeneratorUi.test.ts +103 -0
- package/src/domains/create/renderGeneratorUi.ts +65 -0
- package/src/domains/doctor/formatters/doctor.test.ts +50 -2
- package/src/domains/doctor/formatters/doctor.ts +90 -38
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
- package/src/domains/doctor/operations/checks.test.ts +6 -4
- package/src/domains/doctor/operations/types.ts +23 -1
- package/src/domains/graph/commands/inspect.test.ts +2 -2
- package/src/domains/graph/commands/query.test.ts +5 -3
- package/src/domains/graph/formatters/inspect.ts +7 -7
- package/src/domains/graph/formatters/query.test.ts +87 -0
- package/src/domains/graph/formatters/query.ts +42 -5
- package/src/domains/graph/operations/inspectUri.test.ts +5 -5
- package/src/domains/graphql/commands/serve.test.ts +12 -6
- package/src/domains/graphql/commands/serve.ts +16 -6
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
- package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
- package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
- package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
- package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
- package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
- package/src/domains/graphql/operations/index.ts +5 -0
- package/src/domains/info/formatters/upgrade.test.ts +7 -4
- package/src/domains/info/formatters/upgrade.ts +3 -1
- package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
- package/src/domains/info/operations/collectEntityCounts.ts +123 -0
- package/src/domains/info/operations/index.ts +1 -0
- package/src/domains/info/types.ts +13 -0
- package/src/domains/llm/capabilities.test.ts +23 -3
- package/src/domains/llm/commands/capabilities.ts +12 -3
- package/src/domains/llm/commands/llm.ts +5 -1
- package/src/domains/llm/data/commandReference.ts +20 -11
- package/src/domains/llm/data/decisionTrees.ts +10 -5
- package/src/domains/llm/data/toolCatalog.ts +49 -18
- package/src/domains/llm/formatters/orientation.ts +5 -1
- package/src/domains/llm/llmCommand.test.ts +11 -3
- package/src/domains/llm/mcp/specs.ts +3 -4
- package/src/domains/llm/operations/collectContext.test.ts +64 -0
- package/src/domains/llm/operations/collectContext.ts +7 -17
- package/src/domains/modifier/commands/sample.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +5 -3
- package/src/domains/modifier/stories.ts +9 -0
- package/src/domains/ontology/commands/list.test.ts +5 -3
- package/src/domains/ontology/commands/show.test.ts +85 -9
- package/src/domains/ontology/formatters/index.ts +1 -0
- package/src/domains/ontology/formatters/list.ts +37 -12
- package/src/domains/ontology/formatters/show.ts +358 -67
- package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
- package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
- package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
- package/src/domains/ontology/helpers/index.ts +14 -0
- package/src/domains/ontology/helpers/queryClasses.ts +32 -16
- package/src/domains/ontology/helpers/queryProperties.ts +39 -6
- package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
- package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
- package/src/domains/ontology/operations/listOntologies.ts +126 -30
- package/src/domains/ontology/operations/showOntology.test.ts +131 -12
- package/src/domains/ontology/operations/showOntology.ts +289 -7
- package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
- package/src/domains/ontology/stories.ts +53 -8
- package/src/domains/refs/operations/updateRefs.ts +0 -1
- package/src/domains/setup/commands/index.ts +1 -0
- package/src/domains/setup/commands/setup.ts +70 -0
- package/src/domains/setup/index.ts +7 -3
- package/src/domains/setup/operations/setupLsp.test.ts +3 -2
- package/src/domains/setup/operations/setupLsp.ts +7 -2
- package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
- package/src/domains/shared/bootStore.test.ts +82 -0
- package/src/domains/shared/bootStore.ts +56 -20
- package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
- package/src/domains/shared/extractLocalName.test.ts +3 -3
- package/src/domains/shared/formatters.ts +23 -2
- package/src/domains/shared/listTiers.ts +40 -0
- package/src/domains/shared/loaders/bundledLoader.ts +48 -0
- package/src/domains/shared/loaders/gitLoader.ts +1 -2
- package/src/domains/shared/loaders/localLoader.ts +2 -18
- package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
- package/src/domains/shared/loaders/readPackageDir.ts +46 -8
- package/src/domains/shared/lookupMany.test.ts +15 -0
- package/src/domains/shared/lookupMany.ts +22 -6
- package/src/domains/shared/namespaces.ts +13 -5
- package/src/domains/shared/parseSampleCount.test.ts +47 -0
- package/src/domains/shared/parseSampleCount.ts +45 -0
- package/src/domains/shared/prefixes.test.ts +211 -0
- package/src/domains/shared/prefixes.ts +182 -15
- package/src/domains/shared/renderers.ts +5 -5
- package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
- package/src/domains/shared/resolveLookupExitCode.ts +42 -0
- package/src/domains/shared/semanticPackage.test.ts +89 -0
- package/src/domains/shared/semanticPackage.ts +17 -2
- package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
- package/src/domains/shared/stories/compileLookupTool.ts +1 -1
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
- package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
- package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
- package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
- package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
- package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
- package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
- package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
- package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
- package/src/domains/shared/stories/pack/index.ts +14 -0
- package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
- package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
- package/src/domains/shared/stories/pack/types.ts +123 -3
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
- package/src/domains/shared/stories/types.ts +9 -1
- package/src/domains/shared/types/index.ts +5 -9
- package/src/domains/shared/types/ontology.ts +119 -24
- package/src/domains/shared/types/standard.ts +9 -52
- package/src/domains/token/commands/lookup.test.ts +8 -0
- package/src/domains/token/commands/lookup.ts +12 -0
- package/src/domains/token/commands/sample.ts +2 -1
- package/src/domains/token/featureFlag.ts +17 -0
- package/src/domains/token/index.ts +5 -0
- package/src/domains/token/mcp/specs.ts +13 -1
- package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
- package/src/domains/trace/commands/show.ts +30 -4
- package/src/index.ts +1 -13
- package/src/mcp/registerResources.test.ts +2 -2
- package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
- package/src/mcp/tools/index.ts +18 -10
- package/src/mcp/tools/registerTools.test.ts +106 -30
- package/src/pipeline/collectCommands.test.ts +151 -1
- package/src/pipeline/collectCommands.ts +34 -19
- package/src/pipeline/createProgram.test.ts +5 -1
- package/src/pipeline/createProgram.ts +2 -2
- package/src/pipeline/firstRun.test.ts +80 -0
- package/src/pipeline/firstRun.ts +99 -0
- package/src/pipeline/parseGlobalFlags.test.ts +102 -1
- package/src/pipeline/parseGlobalFlags.ts +98 -9
- package/src/pipeline/resolveCommandKind.test.ts +12 -0
- package/src/pipeline/resolveCommandKind.ts +4 -0
- package/src/pipeline/rootHelp.test.ts +87 -0
- package/src/pipeline/rootHelp.ts +265 -0
- package/src/pipeline/runCli.orchestration.test.ts +28 -12
- package/src/pipeline/runCli.test.ts +17 -4
- package/src/pipeline/runCli.ts +162 -19
- package/src/testing/cli.ts +30 -2
- package/src/testing/dsFixtures.ts +21 -4
- package/src/testing/index.ts +1 -4
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
- package/src/testing/integration/cli-user-stories.test.ts +63 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
- package/src/testing/integration/edge-cases.test.ts +62 -50
- package/src/testing/integration/executor-execution.test.ts +6 -4
- package/src/testing/integration/executor-golden.test.ts +2 -2
- package/src/testing/integration/mcp-surface.test.ts +40 -15
- package/src/testing/integration/parity.test.ts +119 -70
- package/src/testing/setupXdgIsolation.ts +16 -5
- package/src/testing/standardParityFixtures.ts +26 -83
- package/src/testing/store.ts +4 -4
- package/src/domains/create/commands/component.test.ts +0 -166
- package/src/domains/create/commands/component.ts +0 -123
- package/src/domains/create/commands/index.ts +0 -3
- package/src/domains/create/commands/package.test.ts +0 -35
- package/src/domains/create/commands/package.ts +0 -21
- package/src/domains/create/generators.ts +0 -8
- package/src/domains/standard/commands/categories.test.ts +0 -70
- package/src/domains/standard/commands/categories.ts +0 -48
- package/src/domains/standard/commands/index.ts +0 -6
- package/src/domains/standard/commands/list.test.ts +0 -105
- package/src/domains/standard/commands/list.ts +0 -11
- package/src/domains/standard/commands/lookup.test.ts +0 -135
- package/src/domains/standard/commands/lookup.ts +0 -14
- package/src/domains/standard/commands/sample.ts +0 -57
- package/src/domains/standard/formatters/categories.test.ts +0 -44
- package/src/domains/standard/formatters/categories.ts +0 -38
- package/src/domains/standard/formatters/index.ts +0 -10
- package/src/domains/standard/formatters/list.test.ts +0 -169
- package/src/domains/standard/formatters/list.ts +0 -184
- package/src/domains/standard/formatters/lookup.test.ts +0 -95
- package/src/domains/standard/formatters/lookup.ts +0 -101
- package/src/domains/standard/formatters/sample.ts +0 -15
- package/src/domains/standard/formatters/types.ts +0 -31
- package/src/domains/standard/index.ts +0 -40
- package/src/domains/standard/mcp/index.ts +0 -2
- package/src/domains/standard/mcp/specs.ts +0 -89
- package/src/domains/standard/operations/categories.test.ts +0 -61
- package/src/domains/standard/operations/categories.ts +0 -38
- package/src/domains/standard/operations/index.ts +0 -6
- package/src/domains/standard/operations/list.test.ts +0 -75
- package/src/domains/standard/operations/list.ts +0 -64
- package/src/domains/standard/operations/lookup.test.ts +0 -58
- package/src/domains/standard/operations/lookup.ts +0 -151
- package/src/domains/standard/operations/sample.ts +0 -35
- package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
- package/src/domains/standard/orchestration/index.ts +0 -4
- package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
- package/src/domains/standard/standardConfig.ts +0 -42
- package/src/domains/standard/stories.test.ts +0 -24
- package/src/domains/standard/stories.ts +0 -151
- package/src/domains/standard/types.ts +0 -15
- package/src/domains/tier/commands/index.ts +0 -3
- package/src/domains/tier/commands/list.test.ts +0 -67
- package/src/domains/tier/commands/list.ts +0 -9
- package/src/domains/tier/formatters/index.ts +0 -3
- package/src/domains/tier/formatters/list.test.ts +0 -33
- package/src/domains/tier/formatters/list.ts +0 -45
- package/src/domains/tier/index.ts +0 -17
- package/src/domains/tier/mcp/index.ts +0 -2
- package/src/domains/tier/mcp/specs.ts +0 -12
- package/src/domains/tier/operations/index.ts +0 -3
- package/src/domains/tier/operations/list.test.ts +0 -32
- package/src/domains/tier/operations/list.ts +0 -33
- package/src/domains/tier/stories.test.ts +0 -22
- 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
|
-
* -
|
|
13
|
-
* declared values case-insensitively; the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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 =
|
|
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: [...
|
|
106
|
+
validOptions: [...values],
|
|
72
107
|
recovery: {
|
|
73
|
-
message: `Allowed values for --${filter.param}: ${
|
|
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
|
+
};
|