@canonical/pragma-cli 0.35.0 → 0.37.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 +2 -2
- package/bundled-skills/add-standard/SKILL.md +338 -0
- package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
- package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
- package/bundled-skills/anatomy-author/SKILL.md +1226 -0
- package/bundled-skills/design-auditor/SKILL.md +455 -0
- package/bundled-skills/implement-component/SKILL.md +333 -0
- package/bundled-skills/specify-component/SKILL.md +331 -0
- package/bundled-skills/specify-pattern/SKILL.md +292 -0
- package/dist/package.json +24 -20
- package/dist/pragma.conf.js +266 -34
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/bin.js +1 -1
- package/dist/src/bin.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +1 -1
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js +1 -1
- package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/create/mount.js +4 -3
- package/dist/src/capabilities/create/mount.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
- package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/index.js +6 -6
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.render.js +43 -64
- package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
- package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
- package/dist/src/capabilities/doctor/runChecks.js +14 -11
- package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
- package/dist/src/capabilities/graph/query.render.js +2 -2
- package/dist/src/capabilities/graph/query.render.js.map +1 -1
- package/dist/src/capabilities/info/collectInfo.js +8 -2
- package/dist/src/capabilities/info/collectInfo.js.map +1 -1
- package/dist/src/capabilities/info/info.render.js +6 -2
- package/dist/src/capabilities/info/info.render.js.map +1 -1
- package/dist/src/capabilities/prompt/prompt.render.js +2 -2
- package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +43 -39
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/mode.js +2 -2
- package/dist/src/capabilities/setup/operations/index.js +4 -4
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/plan.render.js +144 -66
- package/dist/src/capabilities/setup/plan.render.js.map +1 -1
- package/dist/src/capabilities/setup/setup.render.js +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +165 -63
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +122 -51
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +7 -6
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/capabilities/shared/packageManager.js +300 -54
- package/dist/src/capabilities/shared/packageManager.js.map +1 -1
- package/dist/src/capabilities/skill/discover.js +97 -11
- package/dist/src/capabilities/skill/discover.js.map +1 -1
- package/dist/src/capabilities/skill/render.js +2 -2
- package/dist/src/capabilities/skill/render.js.map +1 -1
- package/dist/src/capabilities/sources/installSkills.js +145 -3
- package/dist/src/capabilities/sources/installSkills.js.map +1 -1
- package/dist/src/capabilities/sources/runUpdate.js +68 -2
- package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
- package/dist/src/capabilities/sources/update.verb.js +3 -3
- package/dist/src/capabilities/sources/update.verb.js.map +1 -1
- package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
- package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
- package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
- package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
- package/dist/src/constants.js +13 -2
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/index.js +11 -2
- package/dist/src/kernel/index.js.map +1 -1
- package/dist/src/kernel/interactivity.js +68 -0
- package/dist/src/kernel/interactivity.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +3 -3
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +63 -12
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +57 -4
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +54 -2
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +46 -24
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/globalFlags.js +4 -1
- package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
- package/dist/src/kernel/project/cli/helpFormat.js +1 -2
- package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +42 -9
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +1 -1
- package/dist/src/kernel/render/style.js +29 -9
- package/dist/src/kernel/render/style.js.map +1 -1
- package/dist/src/kernel/render/vocabulary.js +174 -0
- package/dist/src/kernel/render/vocabulary.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +14 -9
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +9 -3
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +24 -20
- package/pragma.conf.ts +272 -34
- package/src/bin.ts +8 -1
- package/src/capabilities/block.parity.test.ts +25 -11
- package/src/capabilities/block.shipped.exec.test.ts +282 -0
- package/src/capabilities/block.tierRank.exec.test.ts +300 -0
- package/src/capabilities/capabilities/hints.ts +1 -1
- package/src/capabilities/colophon/colophon.render.ts +1 -2
- package/src/capabilities/colophon/colophon.test.ts +2 -2
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +5 -2
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/create/mount.ts +3 -3
- package/src/capabilities/distribution.parity.test.ts +4 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
- package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
- package/src/capabilities/doctor/checks/index.ts +6 -6
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
- package/src/capabilities/doctor/doctor.render.test.ts +108 -32
- package/src/capabilities/doctor/doctor.render.ts +56 -80
- package/src/capabilities/doctor/doctor.test.ts +247 -35
- package/src/capabilities/doctor/doctor.verb.ts +7 -2
- package/src/capabilities/doctor/runChecks.ts +14 -11
- package/src/capabilities/doctor/types.ts +15 -24
- package/src/capabilities/emptyRecovery.test.ts +10 -2
- package/src/capabilities/graph/query.render.ts +2 -2
- package/src/capabilities/info/collectInfo.ts +8 -1
- package/src/capabilities/info/info.render.ts +6 -2
- package/src/capabilities/info/info.test.ts +37 -1
- package/src/capabilities/info/types.ts +10 -2
- package/src/capabilities/lazy.test.ts +26 -2
- package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
- package/src/capabilities/prompt/prompt.render.ts +2 -2
- package/src/capabilities/renderVocabularySync.test.ts +82 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/scopeTypeSync.test.ts +29 -15
- package/src/capabilities/setup/buildPlan.ts +45 -41
- package/src/capabilities/setup/index.ts +1 -1
- package/src/capabilities/setup/mode.ts +2 -2
- package/src/capabilities/setup/operations/index.ts +6 -1
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
- package/src/capabilities/setup/operations/setupLsp.ts +203 -38
- package/src/capabilities/setup/operations/setupMcp.ts +139 -62
- package/src/capabilities/setup/operations/setupSkills.ts +399 -60
- package/src/capabilities/setup/plan.render.ts +171 -76
- package/src/capabilities/setup/plan.ts +17 -12
- package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
- package/src/capabilities/setup/setup.render.test.ts +50 -36
- package/src/capabilities/setup/setup.render.ts +1 -1
- package/src/capabilities/setup/setup.test.ts +1126 -50
- package/src/capabilities/setup/setup.verb.ts +174 -68
- package/src/capabilities/setup/targets.ts +138 -60
- package/src/capabilities/setup/types.ts +10 -18
- package/src/capabilities/shared/index.ts +18 -6
- package/src/capabilities/shared/packageManager.test.ts +421 -6
- package/src/capabilities/shared/packageManager.ts +394 -59
- package/src/capabilities/skill/bundledSkills.test.ts +347 -0
- package/src/capabilities/skill/discover.ts +100 -11
- package/src/capabilities/skill/render.ts +2 -2
- package/src/capabilities/skill/skill.test.ts +17 -4
- package/src/capabilities/sources/installSkills.ts +181 -3
- package/src/capabilities/sources/runUpdate.ts +78 -2
- package/src/capabilities/sources/sources.test.ts +228 -0
- package/src/capabilities/sources/update.verb.ts +3 -3
- package/src/capabilities/surface.test.ts +14 -6
- package/src/capabilities/upgrade/runUpgrade.ts +45 -6
- package/src/capabilities/upgrade/types.ts +10 -2
- package/src/capabilities/upgrade/upgrade.render.ts +9 -2
- package/src/capabilities/upgrade/upgrade.test.ts +72 -0
- package/src/constants.ts +14 -1
- package/src/identity.test.ts +18 -5
- package/src/kernel/completion/entitySource.test.ts +15 -2
- package/src/kernel/completion/safety.test.ts +16 -9
- package/src/kernel/config/defaults.test.ts +19 -3
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/index.ts +11 -2
- package/src/kernel/interactivity.test.ts +76 -0
- package/src/kernel/interactivity.ts +69 -0
- package/src/kernel/packs/compile.test.ts +92 -0
- package/src/kernel/packs/renderPack.ts +3 -3
- package/src/kernel/packs/resolveEntity.test.ts +75 -14
- package/src/kernel/packs/resolveEntity.ts +88 -24
- package/src/kernel/packs/runBodies.ts +84 -19
- package/src/kernel/packs/schema.ts +60 -4
- package/src/kernel/packs/sparql/applyFilters.ts +125 -4
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
- package/src/kernel/packs/toolDescription.test.ts +35 -1
- package/src/kernel/packs/types.ts +125 -2
- package/src/kernel/project/cli/buildProgram.test.ts +73 -2
- package/src/kernel/project/cli/buildProgram.ts +8 -1
- package/src/kernel/project/cli/dispatch.test.ts +5 -4
- package/src/kernel/project/cli/dispatch.ts +48 -25
- package/src/kernel/project/cli/globalFlags.ts +4 -1
- package/src/kernel/project/cli/helpFormat.ts +1 -2
- package/src/kernel/project/cli/rootHelp.ts +44 -8
- package/src/kernel/project/mcp/registerVerb.ts +20 -1
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.ts +1 -1
- package/src/kernel/render/style.ts +32 -9
- package/src/kernel/render/vocabulary.ts +276 -0
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +14 -12
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +9 -3
- package/src/kernel/runtime/types.ts +17 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/types.ts +35 -6
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/docExamples.test.ts +4 -2
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
- package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
- package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
- package/src/testing/eval/cases/readNouns.ts +245 -9
- package/src/testing/fixtures/blockGraph.ts +28 -13
- package/src/testing/fixtures/graph/canonical.ts +95 -12
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/dist/src/capabilities/shared/bands.js +0 -16
- package/dist/src/capabilities/shared/bands.js.map +0 -1
- package/src/capabilities/shared/bands.ts +0 -16
|
@@ -41,6 +41,16 @@ const TERM_PATTERN =
|
|
|
41
41
|
/** Params a filter/verb may not claim (they are the shared read vocabulary). */
|
|
42
42
|
const RESERVED_PARAMS = new Set(["search", "detail", "name", "count"]);
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Whether an author-supplied query is a SELECT (optionally preceded by its own
|
|
46
|
+
* PREFIX lines). Every author query a story declares runs through `runSelect`,
|
|
47
|
+
* which reads `result.bindings` — so a non-SELECT is a shape mismatch caught
|
|
48
|
+
* here rather than downstream.
|
|
49
|
+
*/
|
|
50
|
+
function isSelectQuery(query: string): boolean {
|
|
51
|
+
return /^\s*(?:PREFIX\s+[^\n]*\n\s*)*SELECT\s/i.test(query);
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
const term = z.string().regex(TERM_PATTERN, "must be a prefixed name or IRI");
|
|
45
55
|
const graphqlName = z.string().regex(GRAPHQL_NAME_PATTERN);
|
|
46
56
|
const fieldName = z.string().regex(FIELD_PATTERN);
|
|
@@ -49,16 +59,35 @@ const columnSchema = z
|
|
|
49
59
|
.object({ field: fieldName, label: z.string().optional() })
|
|
50
60
|
.strict();
|
|
51
61
|
|
|
62
|
+
const vocabularySchema = z
|
|
63
|
+
.object({
|
|
64
|
+
query: z.string().min(1),
|
|
65
|
+
variable: fieldName.optional(),
|
|
66
|
+
})
|
|
67
|
+
.strict();
|
|
68
|
+
|
|
52
69
|
const filterSchema = z
|
|
53
70
|
.object({
|
|
54
71
|
param: z.string().regex(FILTER_PARAM_PATTERN),
|
|
55
72
|
variable: fieldName,
|
|
56
73
|
values: z.array(z.string()).min(1).optional(),
|
|
74
|
+
match: z.enum(["exact", "set"]).optional(),
|
|
75
|
+
vocabulary: vocabularySchema.optional(),
|
|
57
76
|
description: z.string().optional(),
|
|
58
77
|
})
|
|
59
78
|
.strict()
|
|
60
79
|
.refine((f) => !RESERVED_PARAMS.has(f.param), {
|
|
61
80
|
message: "filter param is a reserved name",
|
|
81
|
+
})
|
|
82
|
+
// A declared `values` set IS the vocabulary — the filter projects it as an
|
|
83
|
+
// enum and canonicalizes against it. A second, graph-read one alongside would
|
|
84
|
+
// be a silent no-op at best and a disagreement at worst.
|
|
85
|
+
.refine((f) => !(f.values && f.vocabulary), {
|
|
86
|
+
message:
|
|
87
|
+
'"values" and "vocabulary" are mutually exclusive — a declared value set is already the vocabulary',
|
|
88
|
+
})
|
|
89
|
+
.refine((f) => !f.vocabulary || isSelectQuery(f.vocabulary.query), {
|
|
90
|
+
message: '"vocabulary.query" must be a SPARQL SELECT query',
|
|
62
91
|
});
|
|
63
92
|
|
|
64
93
|
const searchSchema = z
|
|
@@ -204,15 +233,31 @@ const completionSchema = z
|
|
|
204
233
|
})
|
|
205
234
|
.strict();
|
|
206
235
|
|
|
236
|
+
/**
|
|
237
|
+
* The declared scope-ranking factor (`PackScopeWeight`). `falloff` is bounded
|
|
238
|
+
* the same way `weights` is — a factor outside 0–1 is a promotion, and this
|
|
239
|
+
* declaration only ever DEMOTES a deeper scope below a shallower one.
|
|
240
|
+
*/
|
|
241
|
+
const scopeWeightSchema = z
|
|
242
|
+
.object({
|
|
243
|
+
via: term,
|
|
244
|
+
by: term,
|
|
245
|
+
falloff: z.number().min(0).max(1),
|
|
246
|
+
asserted: term.optional(),
|
|
247
|
+
})
|
|
248
|
+
.strict();
|
|
249
|
+
|
|
207
250
|
const lookupSchema = z
|
|
208
251
|
.object({
|
|
209
252
|
source: z.enum(["sparql", "graphql"]).optional(),
|
|
210
253
|
by: term,
|
|
254
|
+
nameFallback: z.enum(["iri"]).optional(),
|
|
211
255
|
type: term.optional(),
|
|
212
256
|
description: z.string().optional(),
|
|
213
257
|
toolDescription: z.string().optional(),
|
|
214
258
|
types: z.array(term).min(1).optional(),
|
|
215
259
|
weights: z.record(term, z.number().min(0).max(1)).optional(),
|
|
260
|
+
scopeWeight: scopeWeightSchema.optional(),
|
|
216
261
|
graphqlType: graphqlName.optional(),
|
|
217
262
|
fields: z.array(fieldSchema).min(1).optional(),
|
|
218
263
|
sections: z.array(sectionSchema).min(1).optional(),
|
|
@@ -241,10 +286,7 @@ const definitionSchema = z
|
|
|
241
286
|
message: 'a pack must declare at least one of "list" or "lookup".',
|
|
242
287
|
});
|
|
243
288
|
}
|
|
244
|
-
if (
|
|
245
|
-
def.list &&
|
|
246
|
-
!/^\s*(?:PREFIX\s+[^\n]*\n\s*)*SELECT\s/i.test(def.list.query)
|
|
247
|
-
) {
|
|
289
|
+
if (def.list && !isSelectQuery(def.list.query)) {
|
|
248
290
|
ctx.addIssue({
|
|
249
291
|
code: z.ZodIssueCode.custom,
|
|
250
292
|
message: '"list.query" must be a SPARQL SELECT query.',
|
|
@@ -333,6 +375,20 @@ function refineLookup(
|
|
|
333
375
|
});
|
|
334
376
|
}
|
|
335
377
|
|
|
378
|
+
// An IRI-derived name is only as trustworthy as the class vouching for the
|
|
379
|
+
// entity it came from, and without a class constraint nothing bounds the scan
|
|
380
|
+
// the derived-name population would run. Rejected rather than ignored, for the
|
|
381
|
+
// same reason a stale weight is: a silent no-op is how a declaration that
|
|
382
|
+
// never applied survives review.
|
|
383
|
+
if (lookup.nameFallback !== undefined && !lookup.type && !lookup.types) {
|
|
384
|
+
ctx.addIssue({
|
|
385
|
+
code: z.ZodIssueCode.custom,
|
|
386
|
+
message:
|
|
387
|
+
'"lookup.nameFallback" requires a class constraint ("lookup.type" or "lookup.types") to vouch for the entities it names.',
|
|
388
|
+
path: ["lookup", "nameFallback"],
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
336
392
|
// A weight naming a type the lookup does not address can never apply. Reject
|
|
337
393
|
// it rather than ignore it — a silent no-op is how a stale weight survives a
|
|
338
394
|
// rename (the type moves, the weight stays, and nothing says so).
|
|
@@ -8,15 +8,48 @@
|
|
|
8
8
|
* the input is a free term matched the same way. A row lacking the variable never
|
|
9
9
|
* matches. Several filters combine conjunctively; several values for one
|
|
10
10
|
* filter (a repeated flag) combine as a union.
|
|
11
|
+
*
|
|
12
|
+
* A filter declaring `match: "set"` reads its cell as a space-separated SET and
|
|
13
|
+
* matches any member — the shape a query produces for a dimension a row belongs
|
|
14
|
+
* to several values of at once (a category and its ancestors, say). The
|
|
15
|
+
* comparison is otherwise identical, so exactly one code path decides what a
|
|
16
|
+
* match is.
|
|
17
|
+
*
|
|
18
|
+
* A value-free filter still REJECTS a value the data does not carry, with the
|
|
19
|
+
* admissible values as `validOptions` — the same courtesy a declared-`values`
|
|
20
|
+
* filter has always extended, without copying the vocabulary into the consumer.
|
|
21
|
+
* The vocabulary is read from the graph, because the graph IS the vocabulary; a
|
|
22
|
+
* story that hard-coded the slugs would go stale the first time the data grew a
|
|
23
|
+
* category. Before this, a typo'd `--category` returned
|
|
24
|
+
* `{"ok":true,"data":[],"meta":{}}` — indistinguishable, over MCP, from a
|
|
25
|
+
* genuinely empty category.
|
|
26
|
+
*
|
|
27
|
+
* WHICH part of the graph is the vocabulary matters, and it is NOT the rows.
|
|
28
|
+
* Rows are a population: a value can be perfectly real while no row carries it —
|
|
29
|
+
* `standard categories` lists a category with count 0, `ds:ConceptType` declares
|
|
30
|
+
* "Decision guide" before any concept uses it. Validating against rows called
|
|
31
|
+
* those `INVALID_INPUT`, when the documented answer is a calm empty list. So a
|
|
32
|
+
* filter DECLARES where its vocabulary lives ({@link PackFilter.vocabulary},
|
|
33
|
+
* resolved by the caller and handed in here), and the observed rows are the
|
|
34
|
+
* fallback for a filter that declares none — the only evidence there is, and
|
|
35
|
+
* knowingly narrower than the truth.
|
|
11
36
|
*/
|
|
12
37
|
|
|
13
38
|
import { PragmaError } from "../../error/index.js";
|
|
14
39
|
import type { PackFilter, PackRow } from "../types.js";
|
|
15
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Authoritative values per filter `param`, read from each filter's declared
|
|
43
|
+
* `vocabulary`. A param absent from the map falls back to the observed rows.
|
|
44
|
+
*/
|
|
45
|
+
export type FilterVocabularies = ReadonlyMap<string, readonly string[]>;
|
|
46
|
+
|
|
16
47
|
/**
|
|
17
48
|
* @param rows - Rows produced by the pack's list query.
|
|
18
49
|
* @param filters - Declared filters (absent means no filtering).
|
|
19
50
|
* @param params - Story parameters as provided by the surface.
|
|
51
|
+
* @param vocabularies - Authoritative values per filter param, resolved from
|
|
52
|
+
* each filter's declared `vocabulary` (see {@link FilterVocabularies}).
|
|
20
53
|
* @returns Rows matching every provided filter.
|
|
21
54
|
* @throws PragmaError INVALID_INPUT when a value is not in a filter's declared
|
|
22
55
|
* set, or when a value-free filter receives a non-string value.
|
|
@@ -25,6 +58,7 @@ export function applyPackFilters(
|
|
|
25
58
|
rows: PackRow[],
|
|
26
59
|
filters: readonly PackFilter[] | undefined,
|
|
27
60
|
params: Record<string, unknown>,
|
|
61
|
+
vocabularies?: FilterVocabularies,
|
|
28
62
|
): PackRow[] {
|
|
29
63
|
let result = rows;
|
|
30
64
|
for (const filter of filters ?? []) {
|
|
@@ -41,14 +75,101 @@ export function applyPackFilters(
|
|
|
41
75
|
? requireStringValue(occurrence, filter).toLowerCase()
|
|
42
76
|
: canonicalizeFilterValue(occurrence, filter, values).toLowerCase(),
|
|
43
77
|
);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
// The fallback vocabulary is read from the UNFILTERED rows: a value that
|
|
79
|
+
// exists but is excluded by a conjunction is a legitimately empty answer,
|
|
80
|
+
// not a bad argument.
|
|
81
|
+
if (values === undefined)
|
|
82
|
+
rejectUnknownValue(
|
|
83
|
+
vocabularies?.get(filter.param) ?? observedValues(rows, filter),
|
|
84
|
+
filter,
|
|
85
|
+
occurrences,
|
|
86
|
+
terms,
|
|
87
|
+
);
|
|
88
|
+
result = result.filter((row) =>
|
|
89
|
+
cellValues(row, filter).some((value) =>
|
|
90
|
+
terms.includes(value.toLowerCase()),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
48
93
|
}
|
|
49
94
|
return result;
|
|
50
95
|
}
|
|
51
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The values the rows themselves carry — the fallback vocabulary for a filter
|
|
99
|
+
* that declares no authoritative one, in the data's own DISPLAY spelling.
|
|
100
|
+
*/
|
|
101
|
+
function observedValues(
|
|
102
|
+
rows: readonly PackRow[],
|
|
103
|
+
filter: PackFilter,
|
|
104
|
+
): string[] {
|
|
105
|
+
const observed = new Map<string, string>();
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
for (const value of cellValues(row, filter)) {
|
|
108
|
+
const key = value.toLowerCase();
|
|
109
|
+
if (!observed.has(key)) observed.set(key, value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return [...observed.values()];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reject a value-free filter value the vocabulary does not admit, naming the
|
|
117
|
+
* ones it does.
|
|
118
|
+
*
|
|
119
|
+
* Silent when the vocabulary is EMPTY: an unbuilt or genuinely-empty store is
|
|
120
|
+
* not the caller's mistake, and the list's own `emptyRecovery` is the right
|
|
121
|
+
* voice for it.
|
|
122
|
+
*
|
|
123
|
+
* A value the vocabulary admits is never rejected here even when no row carries
|
|
124
|
+
* it — a real slug with zero standards is a calm empty list, which is what the
|
|
125
|
+
* tool descriptions document and what `standard categories` reporting count 0
|
|
126
|
+
* promises.
|
|
127
|
+
*
|
|
128
|
+
* @throws PragmaError INVALID_INPUT when a term matches none of the admissible
|
|
129
|
+
* values, carrying them as `validOptions`.
|
|
130
|
+
*/
|
|
131
|
+
function rejectUnknownValue(
|
|
132
|
+
vocabulary: readonly string[],
|
|
133
|
+
filter: PackFilter,
|
|
134
|
+
occurrences: readonly unknown[],
|
|
135
|
+
terms: readonly string[],
|
|
136
|
+
): void {
|
|
137
|
+
const admissible = new Map<string, string>();
|
|
138
|
+
for (const value of vocabulary) {
|
|
139
|
+
const key = value.normalize("NFC").toLowerCase();
|
|
140
|
+
if (!admissible.has(key)) admissible.set(key, value.normalize("NFC"));
|
|
141
|
+
}
|
|
142
|
+
if (admissible.size === 0) return;
|
|
143
|
+
const index = terms.findIndex((term) => !admissible.has(term));
|
|
144
|
+
if (index === -1) return;
|
|
145
|
+
const validOptions = [...admissible.values()].sort();
|
|
146
|
+
throw PragmaError.invalidInput(filter.param, String(occurrences[index]), {
|
|
147
|
+
validOptions,
|
|
148
|
+
recovery: {
|
|
149
|
+
message: `Values allowed for --${filter.param}: ${validOptions.join(", ")}.`,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The values a row offers for one filter, in their DISPLAY spelling (NFC).
|
|
156
|
+
*
|
|
157
|
+
* One value for an ordinary filter; every space-separated member for a `"set"`
|
|
158
|
+
* one. A row lacking the variable offers none, so it never matches. Kept in the
|
|
159
|
+
* data's own casing because these are also what an INVALID_INPUT lists back as
|
|
160
|
+
* `validOptions` — lowercasing here would hand the caller a value the graph does
|
|
161
|
+
* not spell that way. Comparison lowercases at the point of comparison instead.
|
|
162
|
+
*/
|
|
163
|
+
function cellValues(row: PackRow, filter: PackFilter): string[] {
|
|
164
|
+
const cell = row[filter.variable];
|
|
165
|
+
if (cell === undefined) return [];
|
|
166
|
+
const normalized = cell.normalize("NFC");
|
|
167
|
+
if (filter.match !== "set") {
|
|
168
|
+
return normalized === "" ? [] : [normalized];
|
|
169
|
+
}
|
|
170
|
+
return normalized.split(/\s+/).filter((value) => value !== "");
|
|
171
|
+
}
|
|
172
|
+
|
|
52
173
|
/** @throws PragmaError INVALID_INPUT when a value-free filter value is not a string. */
|
|
53
174
|
function requireStringValue(provided: unknown, filter: PackFilter): string {
|
|
54
175
|
if (typeof provided !== "string") {
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `?name` binding every generated lookup query shares.
|
|
3
|
+
*
|
|
4
|
+
* The unit under test is narrow but load-bearing: WHAT an entity's name is
|
|
5
|
+
* decides which entities a `lookup` can address at all, and it must agree with
|
|
6
|
+
* what the `list` story publishes. When it did not, `standard list` handed out
|
|
7
|
+
* `react/component/tsdoc` and `standard lookup react/component/tsdoc` answered
|
|
8
|
+
* ENTITY_NOT_FOUND — the two halves of the declared two-step grammar
|
|
9
|
+
* disagreeing about the same entity.
|
|
10
|
+
*
|
|
11
|
+
* The agreement runs in BOTH directions, which is why the fallback is a
|
|
12
|
+
* DECLARATION (`nameFallback: "iri"`) and not an inference from the presence of
|
|
13
|
+
* a class constraint. A story whose list REQUIRES its `by` property publishes no
|
|
14
|
+
* derived name, so deriving one for its lookup would make entities addressable
|
|
15
|
+
* — and sampleable — under names the list never handed out. Both directions are
|
|
16
|
+
* pinned below.
|
|
17
|
+
*
|
|
18
|
+
* The second half of this file is the ranking a name resolve ORDERS BY, pinned
|
|
19
|
+
* as EMITTED TEXT with no store. That is deliberate: the store-backed suites
|
|
20
|
+
* prove which block wins, and a store can only ever prove it for the entities it
|
|
21
|
+
* holds — the emission is what says the rank is computed from a scope's declared
|
|
22
|
+
* NAME rather than enumerated, that an asserted rank would take precedence over
|
|
23
|
+
* the derived one, and that a story declaring no ranking is left exactly as it
|
|
24
|
+
* was. None of those three survive as an assertion about one graph.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, expect, it } from "vitest";
|
|
28
|
+
import type { PackLookup } from "../types.js";
|
|
29
|
+
import {
|
|
30
|
+
buildLookupByIriQuery,
|
|
31
|
+
buildLookupNamesQuery,
|
|
32
|
+
buildLookupQuery,
|
|
33
|
+
buildNameResolveQuery,
|
|
34
|
+
} from "./buildLookupQuery.js";
|
|
35
|
+
|
|
36
|
+
/** The one story that declares the fallback: `standard`, whose list derives names. */
|
|
37
|
+
const DERIVED: PackLookup = {
|
|
38
|
+
source: "sparql",
|
|
39
|
+
by: "cs:name",
|
|
40
|
+
nameFallback: "iri",
|
|
41
|
+
type: "cs:CodeStandard",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A class-constrained lookup that does NOT declare the fallback — the shape
|
|
46
|
+
* `block`/`token`/`tier`/`concept` declare, whose lists require the `by`
|
|
47
|
+
* property.
|
|
48
|
+
*/
|
|
49
|
+
const CONSTRAINED: PackLookup = {
|
|
50
|
+
source: "sparql",
|
|
51
|
+
by: "ds:tokenId",
|
|
52
|
+
type: "ds:Token",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** No class to vouch for `?uri`: the strictest binding of the three. */
|
|
56
|
+
const UNCONSTRAINED: PackLookup = { source: "sparql", by: "ds:name" };
|
|
57
|
+
|
|
58
|
+
describe("the shared ?name binding", () => {
|
|
59
|
+
it("makes the `by` triple OPTIONAL and derives a name when the story declares it", () => {
|
|
60
|
+
const query = buildLookupQuery(DERIVED, "react/component/props");
|
|
61
|
+
expect(query).toContain("?uri a cs:CodeStandard .");
|
|
62
|
+
expect(query).toContain("OPTIONAL { ?uri cs:name ?byName . }");
|
|
63
|
+
expect(query).toContain("BIND(COALESCE(?byName,");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("derives dot-separated local names with slashes — the spelling `list` publishes", () => {
|
|
67
|
+
// `cs:react.component.props` → `react/component/props`. Pinned because the
|
|
68
|
+
// list story performs the SAME derivation to publish a row's name; a change
|
|
69
|
+
// on one side without the other re-opens the unaddressable-name defect.
|
|
70
|
+
expect(buildLookupQuery(DERIVED, "x")).toContain(
|
|
71
|
+
'REPLACE(REPLACE(STR(?uri), "^.*[#/]", ""), "\\\\.", "/")',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("REQUIRES the `by` triple for a class-constrained story that declares no fallback", () => {
|
|
76
|
+
// The narrowing this option exists for. `token list` requires `ds:tokenId`,
|
|
77
|
+
// so a `ds:Token` without one is a row the list never publishes — inferring
|
|
78
|
+
// a derived name from the class constraint alone made it addressable under
|
|
79
|
+
// a name no surface ever handed out.
|
|
80
|
+
const query = buildLookupQuery(CONSTRAINED, "spacing.medium");
|
|
81
|
+
expect(query).toContain("?uri a ds:Token .");
|
|
82
|
+
expect(query).toContain("?uri ds:tokenId ?name .");
|
|
83
|
+
expect(query).not.toContain("COALESCE");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("still REQUIRES the `by` triple when the lookup constrains no class", () => {
|
|
87
|
+
const query = buildLookupQuery(UNCONSTRAINED, "Button");
|
|
88
|
+
expect(query).toContain("?uri ds:name ?name .");
|
|
89
|
+
expect(query).not.toContain("OPTIONAL { ?uri ds:name");
|
|
90
|
+
expect(query).not.toContain("COALESCE");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("ignores a fallback declared without a class to vouch for it", () => {
|
|
94
|
+
// The schema rejects the pairing outright; this is the builders' own guard,
|
|
95
|
+
// because a derived name with no class constraint leaves `?uri` bound by
|
|
96
|
+
// nothing and scans the whole graph.
|
|
97
|
+
const query = buildLookupQuery(
|
|
98
|
+
{ ...UNCONSTRAINED, nameFallback: "iri" },
|
|
99
|
+
"Button",
|
|
100
|
+
);
|
|
101
|
+
expect(query).toContain("?uri ds:name ?name .");
|
|
102
|
+
expect(query).not.toContain("COALESCE");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("binds `?uri` before the BIND that reads it (SPARQL scoping)", () => {
|
|
106
|
+
const query = buildLookupQuery(DERIVED, "x");
|
|
107
|
+
expect(query.indexOf("?uri a cs:CodeStandard .")).toBeLessThan(
|
|
108
|
+
query.indexOf("BIND(COALESCE("),
|
|
109
|
+
);
|
|
110
|
+
const byIri = buildLookupByIriQuery(DERIVED, "https://example.test/a");
|
|
111
|
+
expect(
|
|
112
|
+
byIri.indexOf("BIND(<https://example.test/a> AS ?uri)"),
|
|
113
|
+
).toBeLessThan(byIri.indexOf("BIND(COALESCE("));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("keeps the `by` value a LABEL on the IRI-addressed form", () => {
|
|
117
|
+
// An IRI names the entity by itself, so a class constraint is already a
|
|
118
|
+
// sufficient existence check and the `by` triple is a label to project.
|
|
119
|
+
// Unchanged by the fallback option: a `ds:Token` reached by its IRI still
|
|
120
|
+
// resolves whether or not it carries a `ds:tokenId`.
|
|
121
|
+
const byIri = buildLookupByIriQuery(CONSTRAINED, "https://example.test/a");
|
|
122
|
+
expect(byIri).toContain("OPTIONAL { ?uri ds:tokenId ?name . }");
|
|
123
|
+
expect(byIri).not.toContain("COALESCE");
|
|
124
|
+
// With no class to vouch for it, the triple is the only existence check
|
|
125
|
+
// between a typo'd IRI and an empty entity, so it stays required.
|
|
126
|
+
expect(
|
|
127
|
+
buildLookupByIriQuery(UNCONSTRAINED, "https://example.test/a"),
|
|
128
|
+
).toContain(" ?uri ds:name ?name .");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("uses the SAME binding for the addressable population as for the resolve", () => {
|
|
132
|
+
// Miss-suggestions, glob expansion and `sample`'s draw pool all read this
|
|
133
|
+
// query. A population wider or narrower than the resolve's is how a
|
|
134
|
+
// suggested name could itself miss, and how the advertised
|
|
135
|
+
// `react/component/*` glob expanded over a pool that contained no slashes.
|
|
136
|
+
const names = buildLookupNamesQuery(DERIVED);
|
|
137
|
+
expect(names).toContain("OPTIONAL { ?uri cs:name ?byName . }");
|
|
138
|
+
expect(names).toContain("BIND(COALESCE(?byName,");
|
|
139
|
+
// And the narrowing reaches the draw pool too: an unnamed `ds:Token` is not
|
|
140
|
+
// a candidate `token sample` can draw.
|
|
141
|
+
expect(buildLookupNamesQuery(CONSTRAINED)).toContain(
|
|
142
|
+
"?uri ds:tokenId ?name .",
|
|
143
|
+
);
|
|
144
|
+
expect(buildLookupNamesQuery(CONSTRAINED)).not.toContain("COALESCE");
|
|
145
|
+
expect(buildLookupNamesQuery(UNCONSTRAINED)).toContain(
|
|
146
|
+
"?uri ds:name ?name .",
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The shape the live `block` story declares: several classes, one of them
|
|
153
|
+
* weighted below the rest, and a scope whose own name carries the hierarchy.
|
|
154
|
+
* Copied from `pragma.conf.ts` rather than imported so a change THERE shows up
|
|
155
|
+
* here as a failing assertion about emitted text, not as a silently different
|
|
156
|
+
* query.
|
|
157
|
+
*/
|
|
158
|
+
const RANKED: PackLookup = {
|
|
159
|
+
source: "graphql",
|
|
160
|
+
by: "ds:name",
|
|
161
|
+
types: ["ds:Component", "ds:Pattern", "ds:Layout", "ds:Subcomponent"],
|
|
162
|
+
weights: { "ds:Subcomponent": 0.6 },
|
|
163
|
+
scopeWeight: {
|
|
164
|
+
via: "ds:tier",
|
|
165
|
+
by: "ds:name",
|
|
166
|
+
falloff: 0.2,
|
|
167
|
+
asserted: "ds:tierRank",
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
describe("the ranking a name resolve orders by", () => {
|
|
172
|
+
it("orders by the score and then by a key that cannot tie", () => {
|
|
173
|
+
// Both halves matter. Without DESC(?score) the ranking is not read at all;
|
|
174
|
+
// without the final STR(?uri) two equally-ranked entities are handed back
|
|
175
|
+
// to the store's scan order, which is the defect one layer up.
|
|
176
|
+
for (const query of [
|
|
177
|
+
buildLookupQuery(RANKED, "Button"),
|
|
178
|
+
buildNameResolveQuery(RANKED, "Button"),
|
|
179
|
+
]) {
|
|
180
|
+
expect(query).toContain("ORDER BY DESC(?score) STR(?uri)");
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("returns the rows it ranked instead of discarding them in the store", () => {
|
|
185
|
+
// The lookup still ANSWERS with one entity — that arity is the tool's
|
|
186
|
+
// contract. The limit is gone one layer lower, so the resolver can see what
|
|
187
|
+
// it is choosing between and name it; under `LIMIT 1` the alternatives never
|
|
188
|
+
// left the store and no surface could mention what it had not been told.
|
|
189
|
+
for (const query of [
|
|
190
|
+
buildLookupQuery(RANKED, "Button"),
|
|
191
|
+
buildNameResolveQuery(RANKED, "Button"),
|
|
192
|
+
buildLookupQuery(CONSTRAINED, "spacing.medium"),
|
|
193
|
+
]) {
|
|
194
|
+
expect(query).not.toContain("LIMIT");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("multiplies the two factors rather than tiebreaking between them", () => {
|
|
199
|
+
// The editorial ruling, in the one line that implements it. As a TIEBREAK,
|
|
200
|
+
// the scope would decide before the type ever spoke and a global
|
|
201
|
+
// SUBcomponent would outrank a whole component elsewhere; as a product it
|
|
202
|
+
// does not (0.6 × 1 < 1 × 0.8).
|
|
203
|
+
expect(buildNameResolveQuery(RANKED, "TextInput")).toContain(
|
|
204
|
+
"BIND(IF(?rankType = 0 || ?rankScopeWeight = 0, 0, ?rankType * ?rankScopeWeight) AS ?score)",
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("DERIVES the scope rank from the scope's declared name, never a list of scopes", () => {
|
|
209
|
+
const query = buildNameResolveQuery(RANKED, "Button");
|
|
210
|
+
// The depth is the separator count in the scope's OWN name. The live tiers
|
|
211
|
+
// spell one scope `ds:apps_launchpad` and "Apps/Launchpad" — only the name
|
|
212
|
+
// carries the slash — so reading the IRI would rank every tier at depth 1.
|
|
213
|
+
expect(query).toContain("OPTIONAL { ?rankScope ds:name ?rankScopeName . }");
|
|
214
|
+
expect(query).toContain(
|
|
215
|
+
'BIND(STRLEN(?rankScopeName) - STRLEN(REPLACE(?rankScopeName, "/", "")) AS ?rankDepth)',
|
|
216
|
+
);
|
|
217
|
+
// No tier is NAMED anywhere in the query: a scope added upstream tomorrow
|
|
218
|
+
// is ranked by the same expression, with no edit here or in the config.
|
|
219
|
+
expect(query).not.toContain("apps_launchpad");
|
|
220
|
+
expect(query).not.toContain("ds:global");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("guards the depth-0 product the top scope always hits", () => {
|
|
224
|
+
// oxigraph raises on `0.2 * 0`, and a raising BIND leaves its variable
|
|
225
|
+
// unbound — so the ONE scope that is supposed to win is the one whose
|
|
226
|
+
// weight silently disappears. Pinned as text because it reads like a
|
|
227
|
+
// redundant branch and is not.
|
|
228
|
+
expect(buildNameResolveQuery(RANKED, "Button")).toContain(
|
|
229
|
+
"BIND(1 - IF(?rankDepth = 0, 0, 0.2 * ?rankDepth) AS ?rankDerived)",
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("lets an ASSERTED rank take precedence over the derived one", () => {
|
|
234
|
+
// The retirement path, stated in the query itself: the day the ontology
|
|
235
|
+
// asserts the ranking, the derived depth stops being consulted and the
|
|
236
|
+
// config declaration is deleted with no code change. Asserting the emitted
|
|
237
|
+
// COALESCE is the only way to pin this — the shipped graph asserts no
|
|
238
|
+
// `ds:tierRank` today, which is exactly the state this outlives.
|
|
239
|
+
expect(buildNameResolveQuery(RANKED, "Button")).toContain(
|
|
240
|
+
"BIND(COALESCE(?rankAsserted, IF(?rankDerived < 0, 0, ?rankDerived), 1) AS ?rankScopeWeight)",
|
|
241
|
+
);
|
|
242
|
+
// …and an entity in no scope at all scores a neutral 1, rather than
|
|
243
|
+
// sinking below every entity that has one.
|
|
244
|
+
const { scopeWeight, ...unscoped } = RANKED;
|
|
245
|
+
expect(scopeWeight).toBeDefined();
|
|
246
|
+
expect(buildNameResolveQuery(unscoped, "Button")).not.toContain(
|
|
247
|
+
"?rankScopeWeight",
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("weighs the LOWEST declared type, so a demotion is not cancelled", () => {
|
|
252
|
+
// The same rule the MCP listing's `effectiveWeight` states in prose. Tested
|
|
253
|
+
// lowest-first, so an entity in two weighted classes takes the demotion any
|
|
254
|
+
// membership asked for.
|
|
255
|
+
expect(buildNameResolveQuery(RANKED, "Button")).toContain(
|
|
256
|
+
"BIND(IF(EXISTS { ?uri a ds:Subcomponent }, 0.6, 1) AS ?rankType)",
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("leaves a story that declares NO ranking exactly as it was", () => {
|
|
261
|
+
// Every other noun. The limit is still gone — a name reaching two entities
|
|
262
|
+
// is answered with two on every story — but nothing else is added, and the
|
|
263
|
+
// order stays the total `STR(?uri)` it already had.
|
|
264
|
+
const query = buildLookupQuery(CONSTRAINED, "spacing.medium");
|
|
265
|
+
expect(query).toContain("ORDER BY STR(?uri)");
|
|
266
|
+
expect(query).not.toContain("?score");
|
|
267
|
+
expect(query).not.toContain("?rankType");
|
|
268
|
+
expect(query).not.toContain("LIMIT");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("keeps the IRI-addressed forms at LIMIT 1, unranked", () => {
|
|
272
|
+
// An IRI is one entity by construction, so there is nothing to rank and
|
|
273
|
+
// nothing to discard. Ranking it would be pure cost on the hot path.
|
|
274
|
+
const byIri = buildLookupByIriQuery(RANKED, "https://ds.canonical.com/a");
|
|
275
|
+
expect(byIri).toContain("LIMIT 1");
|
|
276
|
+
expect(byIri).not.toContain("?score");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("does not PROJECT the score it orders by", () => {
|
|
280
|
+
// `?score` is a ranking artefact, and the sparql path spreads its resolve
|
|
281
|
+
// row straight into the entity — projecting it would put a stray number in
|
|
282
|
+
// `--format json` and in the MCP payload.
|
|
283
|
+
expect(buildNameResolveQuery(RANKED, "Button").split("\n")[0]).toBe(
|
|
284
|
+
"SELECT ?uri ?name WHERE {",
|
|
285
|
+
);
|
|
286
|
+
expect(buildLookupQuery(RANKED, "Button").split("\n")[0]).not.toContain(
|
|
287
|
+
"?score",
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
});
|