@canonical/pragma-cli 0.36.0 → 0.38.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 +3 -2
- package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
- package/bundled-skills/anatomy-author/SKILL.md +840 -137
- package/bundled-skills/design-auditor/SKILL.md +133 -9
- package/bundled-skills/implement-component/SKILL.md +192 -23
- package/bundled-skills/specify-component/SKILL.md +124 -12
- package/bundled-skills/specify-pattern/SKILL.md +47 -8
- package/dist/package.json +23 -20
- package/dist/pragma.conf.js +963 -40
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +14 -5
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +33 -5
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/config/show.render.js +30 -2
- package/dist/src/capabilities/config/show.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/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
- 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 +104 -14
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/graph/connect.render.js +101 -0
- package/dist/src/capabilities/graph/connect.render.js.map +1 -0
- package/dist/src/capabilities/graph/connect.types.js +21 -0
- package/dist/src/capabilities/graph/connect.types.js.map +1 -0
- package/dist/src/capabilities/graph/connect.verb.js +147 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
- package/dist/src/capabilities/graph/index.js +5 -3
- package/dist/src/capabilities/graph/index.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +206 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -0
- package/dist/src/capabilities/graph/relationIndex.js +302 -0
- package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
- package/dist/src/capabilities/graph/runConnect.js +456 -0
- package/dist/src/capabilities/graph/runConnect.js.map +1 -0
- package/dist/src/capabilities/graph/runQuery.js +21 -4
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +6 -2
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/operations/index.js +7 -2
- 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 +72 -14
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/operations/writability.js +114 -0
- package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +80 -31
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +124 -42
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/renderError.js +3 -2
- package/dist/src/kernel/error/renderError.js.map +1 -1
- package/dist/src/kernel/error/validOptions.js +41 -0
- package/dist/src/kernel/error/validOptions.js.map +1 -0
- package/dist/src/kernel/packs/compile.js +90 -6
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/cursor.js +135 -0
- package/dist/src/kernel/packs/cursor.js.map +1 -0
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
- package/dist/src/kernel/packs/paging.js +108 -0
- package/dist/src/kernel/packs/paging.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +154 -5
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +118 -19
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +156 -18
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +160 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
- package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
- package/dist/src/kernel/packs/sparql/index.js +24 -14
- package/dist/src/kernel/packs/sparql/index.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
- package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
- package/dist/src/kernel/packs/storyRules.js +151 -0
- package/dist/src/kernel/packs/storyRules.js.map +1 -0
- package/dist/src/kernel/packs/tierScope.js +310 -0
- package/dist/src/kernel/packs/tierScope.js.map +1 -0
- package/dist/src/kernel/packs/types.js +80 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +10 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +104 -3
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +31 -7
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +34 -2
- package/dist/src/kernel/render/renderers.js.map +1 -1
- 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 +17 -10
- 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/readEntity.js +19 -4
- package/dist/src/kernel/runtime/readEntity.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 +38 -5
- 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 +23 -20
- package/pragma.conf.ts +983 -40
- package/src/capabilities/block.parity.test.ts +207 -27
- package/src/capabilities/block.shipped.exec.test.ts +156 -14
- package/src/capabilities/block.tierRank.exec.test.ts +46 -9
- package/src/capabilities/capabilities/catalog.ts +14 -5
- package/src/capabilities/capabilities/hints.ts +40 -5
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/config/set.test.ts +24 -25
- package/src/capabilities/config/show.render.test.ts +9 -4
- package/src/capabilities/config/show.render.ts +30 -2
- 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 +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/distribution.parity.test.ts +12 -11
- package/src/capabilities/distribution.test.ts +77 -7
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
- package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
- 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 +114 -13
- package/src/capabilities/doctor/doctor.test.ts +327 -36
- package/src/capabilities/emptyRecovery.test.ts +84 -28
- package/src/capabilities/graph/connect.render.ts +141 -0
- package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
- package/src/capabilities/graph/connect.test.ts +361 -0
- package/src/capabilities/graph/connect.types.ts +136 -0
- package/src/capabilities/graph/connect.verb.ts +165 -0
- package/src/capabilities/graph/index.ts +5 -3
- package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
- package/src/capabilities/graph/query.test.ts +17 -0
- package/src/capabilities/graph/queryText.test.ts +121 -0
- package/src/capabilities/graph/queryText.ts +240 -0
- package/src/capabilities/graph/relationIndex.ts +372 -0
- package/src/capabilities/graph/runConnect.ts +577 -0
- package/src/capabilities/graph/runQuery.ts +21 -6
- package/src/capabilities/lazy.test.ts +32 -2
- package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
- package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
- package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/buildPlan.ts +7 -1
- package/src/capabilities/setup/operations/index.ts +14 -0
- 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 +93 -15
- package/src/capabilities/setup/operations/setupLsp.ts +544 -85
- package/src/capabilities/setup/operations/setupMcp.ts +212 -4
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/operations/writability.test.ts +151 -0
- package/src/capabilities/setup/operations/writability.ts +135 -0
- package/src/capabilities/setup/plan.ts +11 -0
- package/src/capabilities/setup/setup.test.ts +992 -16
- package/src/capabilities/setup/setup.verb.ts +84 -36
- package/src/capabilities/setup/targets.ts +152 -48
- package/src/capabilities/setup/types.ts +41 -5
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/sources/sources.test.ts +55 -0
- package/src/capabilities/surface.test.ts +59 -11
- package/src/capabilities/tier.parity.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +784 -0
- package/src/capabilities/tokenGraph.cli.test.ts +279 -0
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
- package/src/capabilities/variable.parity.test.ts +448 -0
- package/src/constants.ts +8 -0
- package/src/kernel/completion/shellDrive.test.ts +32 -30
- package/src/kernel/config/defaults.test.ts +21 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/error/renderError.ts +3 -2
- package/src/kernel/error/validOptions.test.ts +57 -0
- package/src/kernel/error/validOptions.ts +45 -0
- package/src/kernel/packs/collect.test.ts +2 -0
- package/src/kernel/packs/compile.test.ts +188 -15
- package/src/kernel/packs/compile.ts +109 -14
- package/src/kernel/packs/cursor.ts +135 -0
- package/src/kernel/packs/filteredEmpty.test.ts +213 -0
- package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
- package/src/kernel/packs/graphqlSource.test.ts +152 -0
- package/src/kernel/packs/listNotice.test.ts +156 -0
- package/src/kernel/packs/paging.ts +110 -0
- package/src/kernel/packs/renderPack.ts +186 -13
- package/src/kernel/packs/resolveEntity.test.ts +62 -3
- package/src/kernel/packs/resolveEntity.ts +186 -26
- package/src/kernel/packs/runBodies.ts +196 -26
- package/src/kernel/packs/schema.ts +180 -13
- package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
- package/src/kernel/packs/sparql/authorQuery.ts +257 -0
- package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
- package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
- package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
- package/src/kernel/packs/sparql/filterValues.ts +182 -0
- package/src/kernel/packs/sparql/index.ts +30 -14
- package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
- package/src/kernel/packs/sparql/runSelect.ts +42 -2
- package/src/kernel/packs/sparql/searchTerm.ts +29 -0
- package/src/kernel/packs/storyRules.ts +180 -0
- package/src/kernel/packs/tierScope.test.ts +487 -0
- package/src/kernel/packs/tierScope.ts +409 -0
- package/src/kernel/packs/toolDescription.test.ts +13 -10
- package/src/kernel/packs/types.ts +311 -9
- package/src/kernel/project/cli/buildProgram.ts +17 -1
- package/src/kernel/project/cli/dispatch.test.ts +87 -0
- package/src/kernel/project/cli/dispatch.ts +114 -4
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/project/cli/suggest.test.ts +35 -0
- package/src/kernel/project/cli/suggestNames.ts +34 -7
- package/src/kernel/project/mcp/registerVerb.ts +5 -1
- package/src/kernel/render/contracts.ts +2 -0
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.test.ts +43 -0
- package/src/kernel/render/renderers.ts +34 -1
- 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 +17 -13
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/readEntity.ts +19 -4
- 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 +39 -5
- package/src/kernel/runtime/types.ts +30 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +17 -0
- package/src/testing/PARITY_GAPS.ts +4 -4
- package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
- package/src/testing/behavioral/completion.test.ts +7 -6
- package/src/testing/behavioral/disclosure.test.ts +39 -37
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
- package/src/testing/behavioral/journeys.cli.test.ts +71 -32
- package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
- package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
- package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
- package/src/testing/behavioral/mcpSurface.test.ts +28 -25
- package/src/testing/behavioral/parity.test.ts +25 -23
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
- package/src/testing/eval/cases/effectful.ts +7 -1
- package/src/testing/eval/cases/readNouns.ts +100 -11
- package/src/testing/fixtures/blockGraph.ts +89 -5
- package/src/testing/fixtures/connectGraph.ts +159 -0
- package/src/testing/fixtures/graph/canonical.ts +128 -32
- package/src/testing/helpers/fsProbe.ts +88 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/src/testing/perf/budgets.test.ts +3 -1
- package/src/testing/perf/budgets.ts +138 -54
- package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
- package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
- package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
- package/src/kernel/packs/sparql/applyFilters.ts +0 -203
- package/src/kernel/packs/sparql/applySearch.ts +0 -33
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a list story's declared filters, its search term, its tier scope and
|
|
3
|
+
* one page's bounds into the SPARQL the store actually runs.
|
|
4
|
+
*
|
|
5
|
+
* WHY IN THE QUERY. Filters used to be predicates over the rows a query had
|
|
6
|
+
* already returned. That was injection-safe and order-preserving — the two
|
|
7
|
+
* properties this module has to keep — but it put filtering on the wrong side
|
|
8
|
+
* of any cap: a `LIMIT` in a story's own text truncates BEFORE the filter runs,
|
|
9
|
+
* so `--category testing` over a capped population answers from whatever
|
|
10
|
+
* happened to be inside the window. Nothing was being truncated yet, because no
|
|
11
|
+
* story declared a cap and the kernel had none to declare; the moment one
|
|
12
|
+
* exists, every declared filter starts deciding over a truncated population.
|
|
13
|
+
* Pagination is that cap, so the two move together and they move inward.
|
|
14
|
+
*
|
|
15
|
+
* The TIER SCOPE is here for that reason and not by analogy: it narrows the
|
|
16
|
+
* 252-row `block list` under a 300-row page today, so a page that started
|
|
17
|
+
* truncating would decide the default scope by where the window fell. It is the
|
|
18
|
+
* one clause that constrains the ENTITY rather than a projected cell — see
|
|
19
|
+
* {@link ListTierScope}.
|
|
20
|
+
*
|
|
21
|
+
* ONE SHAPE, ALWAYS. The author query becomes a sub-select inside a wrapping
|
|
22
|
+
* SELECT that carries the predicates and the page bounds — whether or not there
|
|
23
|
+
* is anything to filter. It has to be a wrap rather than an appended `HAVING`
|
|
24
|
+
* when there IS something: a `match: "set"` filter reads a cell the author's own
|
|
25
|
+
* `GROUP BY` computed (`standard list`'s `GROUP_CONCAT`), so the predicate can
|
|
26
|
+
* only run after aggregation, and the cap only after the predicate.
|
|
27
|
+
*
|
|
28
|
+
* It is a wrap when there is NOTHING to filter, too, and that is a correction.
|
|
29
|
+
* Appending `LIMIT n` to the author's own text is a syntax error for every
|
|
30
|
+
* story whose text already ends in a solution modifier or a trailing `VALUES`
|
|
31
|
+
* block — `LIMIT` after `LIMIT`, after `OFFSET`, after `VALUES` — and because
|
|
32
|
+
* the page has a DEFAULT, that error fired on every call rather than only on
|
|
33
|
+
* paged ones. The shortcut existed to keep the store running, character for
|
|
34
|
+
* character, the query it ran before. Measured over all nine shipped bodies
|
|
35
|
+
* (median of 15 warm store queries per body, both texts, 2026-09-10) the wrap
|
|
36
|
+
* ran between 2.7 ms FASTER and 0.5 ms slower, deltas straddling zero and the
|
|
37
|
+
* one clear win being `block list`, the 252-row body; every body's rows came
|
|
38
|
+
* back byte-identical through the wrap, key order included. So the shortcut
|
|
39
|
+
* bought nothing measurable and cost a whole class of author query. Keeping it
|
|
40
|
+
* would have meant a guard recognising every trailing-modifier shape the
|
|
41
|
+
* grammar admits — more parsing than the wrap itself needs, to reach the
|
|
42
|
+
* answer the wrap already gives.
|
|
43
|
+
*
|
|
44
|
+
* THE AUTHOR'S TEXT IS SPLIT, NEVER EDITED. Two parts of a query cannot live
|
|
45
|
+
* inside a group graph pattern, and {@link ./authorQuery.readAuthorQuery} is
|
|
46
|
+
* what tells them apart:
|
|
47
|
+
*
|
|
48
|
+
* - The PROLOGUE (`PREFIX`, `BASE`) is lifted to the wrapper's own prologue, in
|
|
49
|
+
* the author's order. That is a split at the `SELECT` keyword, not a rewrite:
|
|
50
|
+
* the body is a suffix of the author's text, byte for byte. A COLLIDING
|
|
51
|
+
* prefix resolves the way it always did — the store prepends the pack's own
|
|
52
|
+
* prefix map ahead of whatever it is handed, the author's declaration is
|
|
53
|
+
* emitted after it, and SPARQL's last declaration wins, so an author who
|
|
54
|
+
* redeclares `ds:` shadows the pack's `ds:` exactly as they did before any
|
|
55
|
+
* wrap existed (verified both shapes against the pinned oxigraph). The
|
|
56
|
+
* wrapper declares nothing and names no prefixed term, so it brings no
|
|
57
|
+
* prefix of its own for an author to collide with.
|
|
58
|
+
* - A DATASET clause (`FROM`, `FROM NAMED`) is refused, at declaration and
|
|
59
|
+
* again here. It sits in the MIDDLE of the author's text rather than at its
|
|
60
|
+
* front, so lifting it would mean cutting a span out — an edit, not a split.
|
|
61
|
+
*
|
|
62
|
+
* The wrapper projects the author's own variables, in the author's own order
|
|
63
|
+
* ({@link ./authorQuery}) — `SELECT *` in the wrapper would hand back
|
|
64
|
+
* alphabetised binding keys and change every JSON answer's shape. An author who
|
|
65
|
+
* wrote `SELECT *` themselves gets `SELECT *`, which is the same alphabetisation
|
|
66
|
+
* twice and so preserves their key order; a FILTERABLE story that writes it is
|
|
67
|
+
* refused, because a projection of `*` names nothing to constrain.
|
|
68
|
+
*
|
|
69
|
+
* USER INPUT IS BOUND, NOT SPLICED. Every value a caller supplies reaches the
|
|
70
|
+
* query as a row of a `VALUES` block — the closest thing the store's text-only
|
|
71
|
+
* interface has to a bound parameter — escaped by
|
|
72
|
+
* {@link ./escape.escapeSparqlString}, and the predicate compares against that
|
|
73
|
+
* variable rather than against interpolated text. The `VALUES` block sits
|
|
74
|
+
* inside `FILTER EXISTS` deliberately: joined into the outer group it would
|
|
75
|
+
* MULTIPLY rows, and a repeated flag over a `set` cell carrying two of the
|
|
76
|
+
* requested members would return that row twice.
|
|
77
|
+
*
|
|
78
|
+
* The comparison itself is the row predicate's, transposed:
|
|
79
|
+
* case-insensitively equal for `exact`, membership of a whitespace-separated
|
|
80
|
+
* set for `set`, substring for search, and a cell that is unbound or empty
|
|
81
|
+
* never matches. One divergence is worth naming: the row predicate NFC-
|
|
82
|
+
* normalised both sides, and SPARQL has no normaliser, so the graph's own
|
|
83
|
+
* spelling is compared as it is stored (the caller's side is still normalised,
|
|
84
|
+
* in {@link ./filterValues}).
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
import { PragmaError } from "../../error/index.js";
|
|
88
|
+
import { RESERVED_VARIABLE_PREFIX } from "../types.js";
|
|
89
|
+
import { readAuthorQuery } from "./authorQuery.js";
|
|
90
|
+
import { escapeSparqlString, formatTerm } from "./escape.js";
|
|
91
|
+
|
|
92
|
+
/** One declared filter, with the values a caller actually supplied. */
|
|
93
|
+
export interface ListPredicate {
|
|
94
|
+
/** SELECT variable the filter constrains (without `?`). */
|
|
95
|
+
readonly variable: string;
|
|
96
|
+
/** Whole-cell comparison, or membership of a whitespace-separated set. */
|
|
97
|
+
readonly match: "exact" | "set";
|
|
98
|
+
/**
|
|
99
|
+
* The admitted values, in the graph's own display spelling. Several are a
|
|
100
|
+
* union (the row matches any of them); several FILTERS are a conjunction.
|
|
101
|
+
*/
|
|
102
|
+
readonly terms: readonly string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A declared search, with the term a caller actually supplied. */
|
|
106
|
+
export interface ListSearch {
|
|
107
|
+
/** SELECT variables searched (without `?`). */
|
|
108
|
+
readonly variables: readonly string[];
|
|
109
|
+
/** The normalised search term (non-empty). */
|
|
110
|
+
readonly term: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The tier scope a list is narrowed to, compiled in beside the filters.
|
|
115
|
+
*
|
|
116
|
+
* IN THE QUERY for the same reason every filter is: a scope applied to the rows
|
|
117
|
+
* a page already returned would decide over a truncated population, and `block
|
|
118
|
+
* list` is a 252-row body under a 300-row page today — one product tier more
|
|
119
|
+
* upstream and the default scope would start depending on where the page fell.
|
|
120
|
+
*
|
|
121
|
+
* It constrains the ENTITY rather than a projected tier column. A story's tier
|
|
122
|
+
* column is derived text (`block list` BINDs the IRI's local name), and two of
|
|
123
|
+
* the three scoped stories project no tier at all — `modifier list` and
|
|
124
|
+
* `concept list` publish name and values. The entity variable is the one thing
|
|
125
|
+
* every list-shaped story has in common (`?uri`, the IRI column), and the edge
|
|
126
|
+
* from it to its tier is what the noun already declares.
|
|
127
|
+
*/
|
|
128
|
+
export interface ListTierScope {
|
|
129
|
+
/** The SELECT variable carrying the entity's IRI (without `?`). */
|
|
130
|
+
readonly entity: string;
|
|
131
|
+
/** The entity → tier edge, a validated pack term. */
|
|
132
|
+
readonly via: string;
|
|
133
|
+
/** The in-scope tier IRIs, read from the store (never caller input). */
|
|
134
|
+
readonly tiers: readonly string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One page's bounds, applied inside the query after filtering and ordering. */
|
|
138
|
+
export interface ListWindow {
|
|
139
|
+
/** Maximum rows to return. */
|
|
140
|
+
readonly limit: number;
|
|
141
|
+
/** Rows to skip first. */
|
|
142
|
+
readonly offset: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Everything the generated list query is composed from. */
|
|
146
|
+
export interface ListQueryInput {
|
|
147
|
+
/** The author's SPARQL SELECT, used verbatim (split, never edited). */
|
|
148
|
+
readonly query: string;
|
|
149
|
+
/** The declared filters a caller supplied values for. */
|
|
150
|
+
readonly predicates: readonly ListPredicate[];
|
|
151
|
+
/** The declared search a caller supplied a term for. */
|
|
152
|
+
readonly search?: ListSearch;
|
|
153
|
+
/** The tier scope this read answers under, absent when it answers from all. */
|
|
154
|
+
readonly scope?: ListTierScope;
|
|
155
|
+
/** The page to return. */
|
|
156
|
+
readonly window: ListWindow;
|
|
157
|
+
/** The story's label, for a configuration diagnosis. */
|
|
158
|
+
readonly label: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build the SELECT one page of a list story runs.
|
|
163
|
+
*
|
|
164
|
+
* @param input - The author query, the supplied filters/search, and the page.
|
|
165
|
+
* @returns SPARQL SELECT text composed only from author terms, escaped values
|
|
166
|
+
* and the page's own integers.
|
|
167
|
+
* @throws PragmaError CONFIG_ERROR when the author query cannot be wrapped in a
|
|
168
|
+
* page, when a filterable story's projection cannot be reproduced, or when
|
|
169
|
+
* its query already uses the generated variable prefix. Each names what it
|
|
170
|
+
* found: the same refusal for three different reasons would send an author to
|
|
171
|
+
* the wrong part of their own file.
|
|
172
|
+
*/
|
|
173
|
+
export function buildListQuery(input: ListQueryInput): string {
|
|
174
|
+
const { query, predicates, search, scope, window, label } = input;
|
|
175
|
+
const clauses = [
|
|
176
|
+
...predicates.map((predicate, index) =>
|
|
177
|
+
filterClause(predicate, `${RESERVED_VARIABLE_PREFIX}Filter${index}`),
|
|
178
|
+
),
|
|
179
|
+
...(search
|
|
180
|
+
? [searchClause(search, `${RESERVED_VARIABLE_PREFIX}Search`)]
|
|
181
|
+
: []),
|
|
182
|
+
...(scope && scope.tiers.length > 0
|
|
183
|
+
? [scopeClause(scope, `${RESERVED_VARIABLE_PREFIX}Tier`)]
|
|
184
|
+
: []),
|
|
185
|
+
];
|
|
186
|
+
if (clauses.length > 0 && query.includes(RESERVED_VARIABLE_PREFIX)) {
|
|
187
|
+
throw PragmaError.configError(
|
|
188
|
+
`Story query in ${label} uses the reserved variable prefix "?${RESERVED_VARIABLE_PREFIX}", ` +
|
|
189
|
+
"which the generated filter clauses bind a caller's values to. Rename it.",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const read = readAuthorQuery(query);
|
|
193
|
+
if (!read.ok) {
|
|
194
|
+
throw PragmaError.configError(
|
|
195
|
+
`Story query in ${label} cannot be paged: ${read.reason}. Every list ` +
|
|
196
|
+
"answer is one page, and a page is a wrapping SELECT over the story's own query.",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const { prologue, projection, body } = read.query;
|
|
200
|
+
// The SCOPE clause is deliberately not counted here: it constrains the entity
|
|
201
|
+
// through a graph edge, not a projected cell, and a sub-select's `SELECT *`
|
|
202
|
+
// still makes the entity variable visible to the wrapper. A FILTER is the
|
|
203
|
+
// case that cannot be checked — it names a variable, and `*` names none.
|
|
204
|
+
if (
|
|
205
|
+
(predicates.length > 0 || search !== undefined) &&
|
|
206
|
+
projection === undefined
|
|
207
|
+
) {
|
|
208
|
+
throw PragmaError.configError(
|
|
209
|
+
`Story query in ${label} declares filters or a search, so its SELECT must ` +
|
|
210
|
+
"project its variables by name — the page projects the same names in the " +
|
|
211
|
+
"same order, which `SELECT *` cannot promise.",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return [
|
|
215
|
+
...(prologue === "" ? [] : [prologue]),
|
|
216
|
+
`SELECT ${projection ? projection.map((variable) => `?${variable}`).join(" ") : "*"}`,
|
|
217
|
+
"WHERE {",
|
|
218
|
+
" {",
|
|
219
|
+
body,
|
|
220
|
+
" }",
|
|
221
|
+
...clauses.map((clause) => ` ${clause}`),
|
|
222
|
+
"}",
|
|
223
|
+
modifier(window),
|
|
224
|
+
].join("\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The page as SPARQL solution modifiers (`OFFSET 0` omitted as the no-op it is). */
|
|
228
|
+
function modifier(window: ListWindow): string {
|
|
229
|
+
return window.offset > 0
|
|
230
|
+
? `LIMIT ${window.limit} OFFSET ${window.offset}`
|
|
231
|
+
: `LIMIT ${window.limit}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* One filter as a row predicate that cannot duplicate a row.
|
|
236
|
+
*
|
|
237
|
+
* `FILTER EXISTS` makes the `VALUES` block a per-row existence test: joining it
|
|
238
|
+
* into the group instead would pair each row with every requested value, and a
|
|
239
|
+
* `set` cell carrying two of them would be returned twice.
|
|
240
|
+
*/
|
|
241
|
+
function filterClause(predicate: ListPredicate, bound: string): string {
|
|
242
|
+
const values = predicate.terms
|
|
243
|
+
.map((term) => `"${escapeSparqlString(term)}"`)
|
|
244
|
+
.join(" ");
|
|
245
|
+
const cell = `?${predicate.variable}`;
|
|
246
|
+
const comparison =
|
|
247
|
+
predicate.match === "set"
|
|
248
|
+
? setMembership(cell, bound)
|
|
249
|
+
: `STR(${cell}) != "" && LCASE(STR(${cell})) = LCASE(?${bound})`;
|
|
250
|
+
return `FILTER EXISTS { VALUES ?${bound} { ${values} } FILTER(${comparison}) }`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Membership of a whitespace-separated set, spelled as a padded substring.
|
|
255
|
+
*
|
|
256
|
+
* The cell is a `GROUP_CONCAT` of every value a row belongs to at once — a
|
|
257
|
+
* category and each of its ancestors. Padding both sides with a space is what
|
|
258
|
+
* keeps it a WHOLE-member comparison, so `testing` does not match
|
|
259
|
+
* `testing-unit`; collapsing runs of whitespace first reproduces the row
|
|
260
|
+
* predicate's own `\s+` split, whatever separator the author's aggregate used.
|
|
261
|
+
*
|
|
262
|
+
* TWO WAYS IT IS WIDER THAN THE SPLIT IT REPLACED, both out of reach of every
|
|
263
|
+
* vocabulary the distribution declares and both worth knowing before a
|
|
264
|
+
* vocabulary is widened:
|
|
265
|
+
*
|
|
266
|
+
* - A value that CONTAINS whitespace matches a contiguous RUN of members: cell
|
|
267
|
+
* `"foo bar baz"`, value `"bar baz"` answers true here where
|
|
268
|
+
* `split(/\s+/).includes(value)` answered false. Unreachable today because no
|
|
269
|
+
* admissible value carries whitespace (all 21 `cs:slug` values are clean), and
|
|
270
|
+
* reachable the moment a filter declares `values: ["a b"]` or a graph
|
|
271
|
+
* vocabulary grows a multi-word term.
|
|
272
|
+
* - An EMPTY value matches an EMPTY cell, because `CONTAINS(" ", " ")` is true.
|
|
273
|
+
* The row predicate answered false on an empty cell, and the `exact` branch
|
|
274
|
+
* above still does via its explicit `STR(?c) != ""` guard, which this branch
|
|
275
|
+
* has no equivalent of. Unreachable today because `readFilterVocabularies`
|
|
276
|
+
* drops empty strings from a vocabulary and `rejectUnknownValue` then refuses
|
|
277
|
+
* `""` as unadmitted — i.e. it is the ADMISSION step that closes this, not the
|
|
278
|
+
* predicate.
|
|
279
|
+
*/
|
|
280
|
+
function setMembership(cell: string, bound: string): string {
|
|
281
|
+
const members = `CONCAT(" ", REPLACE(LCASE(STR(${cell})), "\\\\s+", " "), " ")`;
|
|
282
|
+
return `CONTAINS(${members}, CONCAT(" ", LCASE(?${bound}), " "))`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The tier scope as a row predicate: the entity is in an in-scope tier, or in
|
|
287
|
+
* no tier at all.
|
|
288
|
+
*
|
|
289
|
+
* `EXISTS` with the tiers in a `VALUES` block, for both reasons the filter
|
|
290
|
+
* clauses use that shape. It cannot MULTIPLY a row (an entity in two in-scope
|
|
291
|
+
* tiers is still one entity — the design system asserts one `ds:tier` per block
|
|
292
|
+
* today, and a second one tomorrow must not double the row), and the tier IRIs
|
|
293
|
+
* ride a bound variable rather than being spliced into a pattern.
|
|
294
|
+
*
|
|
295
|
+
* The IRIs are the store's own, read back from the tier hierarchy by
|
|
296
|
+
* {@link ../tierScope.readTierHierarchy} — a caller supplies a tier NAME, which
|
|
297
|
+
* is resolved to a tier before it ever reaches here, so the only thing
|
|
298
|
+
* interpolated is a term the graph itself published.
|
|
299
|
+
*
|
|
300
|
+
* THE UNTIERED HALF IS NOT A COURTESY. `ds:button.icon` is a subcomponent with
|
|
301
|
+
* no `ds:tier`, and a required tier join is exactly what used to hide it: the
|
|
302
|
+
* hand-written block list inner-joined the tier and surfaced it only under
|
|
303
|
+
* `--all-tiers`, which is the defect the declared query's OPTIONAL join closed
|
|
304
|
+
* and `block.parity.test.ts` holds it to. A membership test alone would reopen
|
|
305
|
+
* it — an entity the scope cannot PLACE would become an entity the default
|
|
306
|
+
* scope cannot SHOW. So the predicate is "in scope, or in no tier", which is
|
|
307
|
+
* the same rule the lookup resolver applies to the same rows
|
|
308
|
+
* ({@link ../resolveEntity.applyScope}).
|
|
309
|
+
*
|
|
310
|
+
* `IN` would read closer to the rule as stated, and this is the same predicate:
|
|
311
|
+
* membership of a set, tested per row. A `VALUES` block is how every other
|
|
312
|
+
* generated clause in this module spells a set, so the scope spells it that way
|
|
313
|
+
* too rather than introducing a second idiom for one clause.
|
|
314
|
+
*/
|
|
315
|
+
function scopeClause(scope: ListTierScope, bound: string): string {
|
|
316
|
+
const values = scope.tiers.map((iri) => `<${iri}>`).join(" ");
|
|
317
|
+
const entity = `?${scope.entity}`;
|
|
318
|
+
const via = formatTerm(scope.via);
|
|
319
|
+
return (
|
|
320
|
+
`FILTER(EXISTS { VALUES ?${bound} { ${values} } ${entity} ${via} ?${bound} } ` +
|
|
321
|
+
`|| NOT EXISTS { ${entity} ${via} ?${bound}Any })`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Search as a substring test over every declared variable.
|
|
327
|
+
*
|
|
328
|
+
* `COALESCE(…, false)` per variable rather than one bare disjunction: an
|
|
329
|
+
* unbound cell makes `CONTAINS` raise, and an error inside `||` is not reliably
|
|
330
|
+
* false. The row predicate treated a missing cell as "does not contain", and so
|
|
331
|
+
* does this.
|
|
332
|
+
*/
|
|
333
|
+
function searchClause(search: ListSearch, bound: string): string {
|
|
334
|
+
const tests = search.variables
|
|
335
|
+
.map(
|
|
336
|
+
(variable) =>
|
|
337
|
+
`COALESCE(CONTAINS(LCASE(STR(?${variable})), LCASE(?${bound})), false)`,
|
|
338
|
+
)
|
|
339
|
+
.join(" || ");
|
|
340
|
+
return (
|
|
341
|
+
`FILTER EXISTS { VALUES ?${bound} { "${escapeSparqlString(search.term)}" } ` +
|
|
342
|
+
`FILTER(${tests}) }`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { describe, expect, it } from "vitest";
|
|
28
28
|
import type { PackLookup } from "../types.js";
|
|
29
29
|
import {
|
|
30
|
+
buildExpandQuery,
|
|
30
31
|
buildLookupByIriQuery,
|
|
31
32
|
buildLookupNamesQuery,
|
|
32
33
|
buildLookupQuery,
|
|
@@ -168,6 +169,54 @@ const RANKED: PackLookup = {
|
|
|
168
169
|
},
|
|
169
170
|
};
|
|
170
171
|
|
|
172
|
+
describe("the name FILTER a name-addressed resolve matches by", () => {
|
|
173
|
+
// Pinned as EMITTED TEXT because no store can prove the second half. A
|
|
174
|
+
// fixture graph shows that a padded name resolves (`packs/resolveEntity.test`
|
|
175
|
+
// does, over an entity named "Timeline " the way 66 shipped names are); only
|
|
176
|
+
// the emission says that the ARGUMENT is trimmed too, and that the regex is
|
|
177
|
+
// the two anchored runs rather than something that would also eat the spaces
|
|
178
|
+
// inside "Button Icon".
|
|
179
|
+
const TRIMMED =
|
|
180
|
+
'FILTER (LCASE(REPLACE(STR(?name), "^\\\\s+|\\\\s+$", "")) = LCASE(';
|
|
181
|
+
|
|
182
|
+
it("compares the name with BOTH sides trimmed, on both name forms", () => {
|
|
183
|
+
for (const query of [
|
|
184
|
+
buildLookupQuery(RANKED, "Timeline"),
|
|
185
|
+
buildNameResolveQuery(RANKED, "Timeline"),
|
|
186
|
+
buildLookupQuery(CONSTRAINED, "spacing.medium"),
|
|
187
|
+
]) {
|
|
188
|
+
expect(query).toContain(`${TRIMMED}"`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("trims the ARGUMENT in TypeScript rather than asking the store to", () => {
|
|
193
|
+
// The store side can only trim `?name`. A padded ARGUMENT — a name pasted
|
|
194
|
+
// out of a table cell — has to be trimmed before it becomes a literal, or
|
|
195
|
+
// `lookup "Button "` still misses an entity whose own name is clean.
|
|
196
|
+
expect(buildLookupQuery(RANKED, " Timeline\t")).toContain(
|
|
197
|
+
`${TRIMMED}"Timeline"))`,
|
|
198
|
+
);
|
|
199
|
+
expect(buildNameResolveQuery(RANKED, "\nTimeline ")).toContain(
|
|
200
|
+
`${TRIMMED}"Timeline"))`,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("strips only the ENDS, leaving the spaces a name carries inside it", () => {
|
|
205
|
+
// `Button Icon` is a live block name. A whitespace-collapsing REPLACE would
|
|
206
|
+
// make it unaddressable, so the anchors are the whole point of the pattern.
|
|
207
|
+
const query = buildLookupQuery(RANKED, "Button Icon");
|
|
208
|
+
expect(query).toContain(`${TRIMMED}"Button Icon"))`);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("leaves the IRI-addressed forms with no name FILTER at all", () => {
|
|
212
|
+
// An IRI is the address; `?name` is a label to project there, and filtering
|
|
213
|
+
// on it would re-impose the requirement the IRI form exists to avoid.
|
|
214
|
+
expect(
|
|
215
|
+
buildLookupByIriQuery(RANKED, "https://ds.canonical.com/a"),
|
|
216
|
+
).not.toContain("FILTER (LCASE(");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
171
220
|
describe("the ranking a name resolve orders by", () => {
|
|
172
221
|
it("orders by the score and then by a key that cannot tie", () => {
|
|
173
222
|
// Both halves matter. Without DESC(?score) the ranking is not read at all;
|
|
@@ -288,3 +337,92 @@ describe("the ranking a name resolve orders by", () => {
|
|
|
288
337
|
);
|
|
289
338
|
});
|
|
290
339
|
});
|
|
340
|
+
|
|
341
|
+
describe("the expand sub-SELECT", () => {
|
|
342
|
+
const BUTTON = "https://ds.canonical.com/global.component.button";
|
|
343
|
+
const BLOCK: PackLookup = {
|
|
344
|
+
source: "graphql",
|
|
345
|
+
by: "ds:name",
|
|
346
|
+
type: "ds:Component",
|
|
347
|
+
graphqlType: "UIBlock",
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
it("reads each selected field through one OPTIONAL, and orders by nothing", () => {
|
|
351
|
+
// The shape every expand had before an ordering could be declared: rows
|
|
352
|
+
// arrive in the store's own scan order, which SPARQL does not define.
|
|
353
|
+
const query = buildExpandQuery(
|
|
354
|
+
{
|
|
355
|
+
name: "properties",
|
|
356
|
+
relation: "ds:hasProperty",
|
|
357
|
+
select: [{ name: "n", property: "ds:name" }],
|
|
358
|
+
},
|
|
359
|
+
BUTTON,
|
|
360
|
+
BLOCK,
|
|
361
|
+
);
|
|
362
|
+
expect(query).toContain(` <${BUTTON}> ds:hasProperty ?child .`);
|
|
363
|
+
expect(query).toContain(" OPTIONAL { ?child ds:name ?n . }");
|
|
364
|
+
expect(query).not.toContain("ORDER BY");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("emits the declared ordering, in the declared order", () => {
|
|
368
|
+
const query = buildExpandQuery(
|
|
369
|
+
{
|
|
370
|
+
name: "tokens",
|
|
371
|
+
relation: "ds:hasTokenBinding",
|
|
372
|
+
orderBy: ["node", "key", "rank"],
|
|
373
|
+
select: [
|
|
374
|
+
{ name: "key", property: "anatomy:styleKey" },
|
|
375
|
+
{ name: "rank", property: "ds:rank" },
|
|
376
|
+
{ name: "node", property: "ds:node" },
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
BUTTON,
|
|
380
|
+
BLOCK,
|
|
381
|
+
);
|
|
382
|
+
expect(query).toContain("ORDER BY ?node ?key ?rank");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("keeps a blankWhenSelf field's row and UNBINDS the cell when it is the entity's own", () => {
|
|
386
|
+
// Inside the OPTIONAL, which is the whole behaviour: a FILTER outside it
|
|
387
|
+
// would drop the child row entirely, and what is wanted is the row with an
|
|
388
|
+
// empty cell. Compared as strings because the entity's identity value is a
|
|
389
|
+
// literal and so is the cell.
|
|
390
|
+
const query = buildExpandQuery(
|
|
391
|
+
{
|
|
392
|
+
name: "tokens",
|
|
393
|
+
relation: "ds:hasTokenBinding",
|
|
394
|
+
select: [
|
|
395
|
+
{ name: "via", property: "ds:viaBlock/ds:name", blankWhenSelf: true },
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
BUTTON,
|
|
399
|
+
BLOCK,
|
|
400
|
+
);
|
|
401
|
+
expect(query).toContain(
|
|
402
|
+
" OPTIONAL {\n ?child ds:viaBlock/ds:name ?via .",
|
|
403
|
+
);
|
|
404
|
+
expect(query).toContain(` <${BUTTON}> ds:name ?__pragmaSelf .`);
|
|
405
|
+
expect(query).toContain(" FILTER(STR(?via) != STR(?__pragmaSelf))");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("interpolates ONLY the resolved entity IRI", () => {
|
|
409
|
+
// Injection safety, unchanged by either addition: the relation, the
|
|
410
|
+
// properties, the ordering names and the identity property are all
|
|
411
|
+
// validated pack terms, and the IRI is the one the base lookup already
|
|
412
|
+
// resolved from the store.
|
|
413
|
+
const query = buildExpandQuery(
|
|
414
|
+
{
|
|
415
|
+
name: "tokens",
|
|
416
|
+
relation: "ds:hasTokenBinding",
|
|
417
|
+
orderBy: ["node"],
|
|
418
|
+
select: [
|
|
419
|
+
{ name: "node", property: "ds:node" },
|
|
420
|
+
{ name: "via", property: "ds:viaBlock/ds:name", blankWhenSelf: true },
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
BUTTON,
|
|
424
|
+
BLOCK,
|
|
425
|
+
);
|
|
426
|
+
expect(query.match(/<[^>]+>/g)).toEqual([`<${BUTTON}>`, `<${BUTTON}>`]);
|
|
427
|
+
});
|
|
428
|
+
});
|