@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
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* and which properties to read; the query text is generated here so
|
|
6
6
|
* user-supplied names are always escaped SPARQL string literals, and level-gated
|
|
7
7
|
* fields below the active level are excluded from the projection (fetch-gating).
|
|
8
|
-
* Matching on the name is exact and case
|
|
8
|
+
* Matching on the name is exact, and insensitive to case and to surrounding
|
|
9
|
+
* whitespace — see {@link nameFilter}.
|
|
9
10
|
*
|
|
10
11
|
* WHAT an entity's name IS lives in one place — {@link nameBinding} — and every
|
|
11
12
|
* form here uses it: the asserted `by` value, plus, for a lookup that DECLARES
|
|
@@ -49,9 +50,49 @@
|
|
|
49
50
|
*/
|
|
50
51
|
|
|
51
52
|
import { activeExpands, activeFields } from "../disclosure.js";
|
|
52
|
-
import
|
|
53
|
+
import {
|
|
54
|
+
type PackExpand,
|
|
55
|
+
type PackExpandField,
|
|
56
|
+
type PackLookup,
|
|
57
|
+
RESERVED_VARIABLE_PREFIX,
|
|
58
|
+
} from "../types.js";
|
|
53
59
|
import { escapeSparqlString, formatTerm } from "./escape.js";
|
|
54
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The variable a scoped name resolve reports each match's tier in.
|
|
63
|
+
*
|
|
64
|
+
* Under the reserved prefix because it is the kernel's, not the story's: it
|
|
65
|
+
* exists so the resolver can tell an in-scope match from an out-of-scope one,
|
|
66
|
+
* and it is stripped before an entity is handed to a formatter
|
|
67
|
+
* ({@link ../resolveEntity}) so no answer grows a field a caller did not ask
|
|
68
|
+
* for.
|
|
69
|
+
*/
|
|
70
|
+
export const SCOPE_TIER_VARIABLE = "__pragmaTier";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Report each match's tier, for a name resolve over a SCOPED noun.
|
|
74
|
+
*
|
|
75
|
+
* A name resolve RANKS and returns everything the name reaches, so the scope
|
|
76
|
+
* does not need to be a filter here — the resolver prefers the in-scope matches
|
|
77
|
+
* and falls back to the rest, which is what lets `block lookup button` answer
|
|
78
|
+
* the global Button while still being able to answer at all for a name that
|
|
79
|
+
* lives only in one product's tier. All it needs from the query is WHICH tier
|
|
80
|
+
* each match belongs to.
|
|
81
|
+
*
|
|
82
|
+
* `OPTIONAL`, so an entity with no tier is still reached: not being in any tier
|
|
83
|
+
* is not a reason to be unaddressable by name.
|
|
84
|
+
*/
|
|
85
|
+
function scopeTierProjection(via: string | undefined): {
|
|
86
|
+
variable: string;
|
|
87
|
+
optional: string;
|
|
88
|
+
} {
|
|
89
|
+
if (via === undefined) return { variable: "", optional: "" };
|
|
90
|
+
return {
|
|
91
|
+
variable: ` ?${SCOPE_TIER_VARIABLE}`,
|
|
92
|
+
optional: ` OPTIONAL { ?uri ${formatTerm(via)} ?${SCOPE_TIER_VARIABLE} . }`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
55
96
|
/**
|
|
56
97
|
* The class-constraint clause for a lookup: a single `a` triple for `type`, a
|
|
57
98
|
* VALUES-constrained type triple for `types`, or nothing. All terms are
|
|
@@ -121,13 +162,17 @@ const DERIVED_NAME = 'REPLACE(REPLACE(STR(?uri), "^.*[#/]", ""), "\\\\.", "/")';
|
|
|
121
162
|
*
|
|
122
163
|
* A story whose `list` SYNTHESIZES a name instead of reading one declares
|
|
123
164
|
* `nameFallback: "iri"`, and then the triple becomes OPTIONAL with
|
|
124
|
-
* {@link DERIVED_NAME} behind it. `standard` is the case that exists
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* `
|
|
129
|
-
* answered ENTITY_NOT_FOUND with
|
|
130
|
-
* addressable population was the ~13%
|
|
165
|
+
* {@link DERIVED_NAME} behind it. `standard` is the case that exists. When the
|
|
166
|
+
* defect was measured, 22 of the then-156 live code standards carried a
|
|
167
|
+
* `cs:name`, and the code-standards ontology said so deliberately — "an
|
|
168
|
+
* optional human-readable display title" that "never participates in
|
|
169
|
+
* identity". `standard list` published `react/component/tsdoc` while
|
|
170
|
+
* `standard lookup react/component/tsdoc` answered ENTITY_NOT_FOUND with
|
|
171
|
+
* empty suggestions, because the only addressable population was the ~13%
|
|
172
|
+
* carrying an asserted name. (Upstream has since retired `cs:name` for
|
|
173
|
+
* `rdfs:label`, which every standard carries; the fallback stays because a
|
|
174
|
+
* pack is data, and a dropped label must degrade to the derived name rather
|
|
175
|
+
* than become unaddressable.)
|
|
131
176
|
*
|
|
132
177
|
* INFERRING the fallback from the mere PRESENCE of a class constraint is what
|
|
133
178
|
* this option replaced, and it was over-reach in both directions of the same
|
|
@@ -344,12 +389,43 @@ function rankingClause(lookup: PackLookup): {
|
|
|
344
389
|
};
|
|
345
390
|
}
|
|
346
391
|
|
|
392
|
+
/**
|
|
393
|
+
* The name FILTER both name-addressed resolves share.
|
|
394
|
+
*
|
|
395
|
+
* A name is matched insensitively to case AND to the whitespace it is padded
|
|
396
|
+
* with, because the padding is an artefact of where the name came from rather
|
|
397
|
+
* than part of the name. Sixty-six `ds:name` literals in the shipped graph end
|
|
398
|
+
* in a space, copied across from the source document, and `block lookup
|
|
399
|
+
* Timeline` answered ENTITY_NOT_FOUND while offering "Timeline " straight back
|
|
400
|
+
* as a suggestion — the query and the answer differing by a character no
|
|
401
|
+
* surface prints and no user can type deliberately. The upstream transform is
|
|
402
|
+
* being corrected separately; a resolve a stray space can defeat is a defect on
|
|
403
|
+
* its own, and this is where that half is fixed.
|
|
404
|
+
*
|
|
405
|
+
* BOTH sides are trimmed, never one: the store side by REPLACE over the two
|
|
406
|
+
* anchored runs, the argument side in TypeScript before it is escaped into the
|
|
407
|
+
* literal. Trimming only `?name` would leave `block lookup "Button "` — a name
|
|
408
|
+
* pasted out of a table cell — missing an entity whose own name is clean.
|
|
409
|
+
*
|
|
410
|
+
* The regex the engine reads is `^\s+|\s+$` — the doubled backslashes in the
|
|
411
|
+
* source are the SPARQL string-literal escape and not part of the pattern.
|
|
412
|
+
* Verified against the pinned oxigraph, whose REPLACE takes both the `\s`
|
|
413
|
+
* class and the alternation, and whose `^`/`$` anchor the whole value rather
|
|
414
|
+
* than each line of it.
|
|
415
|
+
*
|
|
416
|
+
* @param name - The user-supplied name, untrimmed and unescaped.
|
|
417
|
+
* @returns The FILTER line to splice into the WHERE clause.
|
|
418
|
+
*/
|
|
419
|
+
function nameFilter(name: string): string {
|
|
420
|
+
return ` FILTER (LCASE(REPLACE(STR(?name), "^\\\\s+|\\\\s+$", "")) = LCASE("${escapeSparqlString(name.trim())}"))`;
|
|
421
|
+
}
|
|
422
|
+
|
|
347
423
|
/**
|
|
348
424
|
* Build the SELECT retrieving the entities a name reaches, with their declared
|
|
349
425
|
* fields, best first. The caller answers with the first and names the rest.
|
|
350
426
|
*
|
|
351
427
|
* @param lookup - The pack's lookup declaration.
|
|
352
|
-
* @param name - User-supplied entity name (escaped here).
|
|
428
|
+
* @param name - User-supplied entity name (trimmed and escaped here).
|
|
353
429
|
* @param level - Active canonical level; gated fields below it are excluded.
|
|
354
430
|
* @returns SPARQL SELECT text, ranked and unlimited.
|
|
355
431
|
*/
|
|
@@ -357,16 +433,19 @@ export function buildLookupQuery(
|
|
|
357
433
|
lookup: PackLookup,
|
|
358
434
|
name: string,
|
|
359
435
|
level?: string,
|
|
436
|
+
scopeVia?: string,
|
|
360
437
|
): string {
|
|
361
438
|
const { header, constraint, optionals } = lookupProjection(lookup, level);
|
|
362
439
|
const ranking = rankingClause(lookup);
|
|
440
|
+
const scope = scopeTierProjection(scopeVia);
|
|
363
441
|
return [
|
|
364
|
-
header,
|
|
442
|
+
header.replace(" WHERE {", `${scope.variable} WHERE {`),
|
|
365
443
|
constraint,
|
|
366
444
|
nameBinding(lookup),
|
|
367
445
|
ranking.binds,
|
|
368
446
|
optionals,
|
|
369
|
-
|
|
447
|
+
scope.optional,
|
|
448
|
+
nameFilter(name),
|
|
370
449
|
"}",
|
|
371
450
|
ranking.orderBy,
|
|
372
451
|
]
|
|
@@ -410,7 +489,7 @@ export function buildLookupByIriQuery(
|
|
|
410
489
|
* Build the minimal name→URI resolve for a graphql-sourced lookup (and the
|
|
411
490
|
* shared entry point for the sparql path's name form): maps the user-supplied
|
|
412
491
|
* name to the entity IRIs, everything else comes from the field fetch. The name
|
|
413
|
-
* is
|
|
492
|
+
* is a trimmed, escaped literal; all terms are validated pack terms.
|
|
414
493
|
*
|
|
415
494
|
* Ranked and unlimited, exactly like {@link buildLookupQuery} — the two paths
|
|
416
495
|
* differ in where the VALUES come from, never in which entities a name reaches.
|
|
@@ -421,14 +500,17 @@ export function buildLookupByIriQuery(
|
|
|
421
500
|
export function buildNameResolveQuery(
|
|
422
501
|
lookup: PackLookup,
|
|
423
502
|
name: string,
|
|
503
|
+
scopeVia?: string,
|
|
424
504
|
): string {
|
|
425
505
|
const ranking = rankingClause(lookup);
|
|
506
|
+
const scope = scopeTierProjection(scopeVia);
|
|
426
507
|
return [
|
|
427
|
-
|
|
508
|
+
`SELECT ?uri ?name${scope.variable} WHERE {`,
|
|
428
509
|
buildTypeConstraint(lookup).trimEnd(),
|
|
429
510
|
nameBinding(lookup),
|
|
430
511
|
ranking.binds,
|
|
431
|
-
|
|
512
|
+
scope.optional,
|
|
513
|
+
nameFilter(name),
|
|
432
514
|
"}",
|
|
433
515
|
ranking.orderBy,
|
|
434
516
|
]
|
|
@@ -479,27 +561,65 @@ export function buildLookupIrisQuery(lookup: PackLookup): string {
|
|
|
479
561
|
*
|
|
480
562
|
* Injection-safe: the only interpolated value is `entityUri`, the IRI the base
|
|
481
563
|
* lookup already resolved from the store (never user input); the
|
|
482
|
-
* relation/properties
|
|
483
|
-
*
|
|
564
|
+
* relation/properties, the ordering names and the entity's identity property
|
|
565
|
+
* are validated pack terms. SPARQL expands are single-hop, so every select
|
|
566
|
+
* entry here is a plain field.
|
|
567
|
+
*
|
|
568
|
+
* `lookup` is read for ONE thing — the entity's own identity property, which a
|
|
569
|
+
* `blankWhenSelf` field compares its value against. A lookup declaring no such
|
|
570
|
+
* field never binds it.
|
|
484
571
|
*/
|
|
485
572
|
export function buildExpandQuery(
|
|
486
573
|
expand: PackExpand,
|
|
487
574
|
entityUri: string,
|
|
575
|
+
lookup?: Pick<PackLookup, "by">,
|
|
488
576
|
): string {
|
|
489
577
|
const vars = expand.select.map((field) => `?${field.name}`).join(" ");
|
|
490
578
|
const optionals = expand.select
|
|
491
579
|
.map((field) =>
|
|
492
|
-
"property" in field
|
|
493
|
-
? ` OPTIONAL { ?child ${formatTerm(field.property)} ?${field.name} . }`
|
|
494
|
-
: "",
|
|
580
|
+
"property" in field ? expandFieldClause(field, entityUri, lookup) : "",
|
|
495
581
|
)
|
|
496
582
|
.filter((line) => line !== "")
|
|
497
583
|
.join("\n");
|
|
584
|
+
const order =
|
|
585
|
+
expand.orderBy && expand.orderBy.length > 0
|
|
586
|
+
? `ORDER BY ${expand.orderBy.map((name) => `?${name}`).join(" ")}`
|
|
587
|
+
: "";
|
|
498
588
|
return [
|
|
499
589
|
`SELECT ${vars} WHERE {`,
|
|
500
590
|
` <${entityUri}> ${formatTerm(expand.relation)} ?child .`,
|
|
501
591
|
optionals,
|
|
502
592
|
"}",
|
|
593
|
+
order,
|
|
594
|
+
]
|
|
595
|
+
.filter((line) => line !== "")
|
|
596
|
+
.join("\n");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The OPTIONAL clause reading one child field.
|
|
601
|
+
*
|
|
602
|
+
* A plain field is one line. A `blankWhenSelf` field binds the ENTITY's own
|
|
603
|
+
* identity value beside the child's and keeps the row only where the two
|
|
604
|
+
* differ — inside the OPTIONAL, so a match leaves the variable UNBOUND (the
|
|
605
|
+
* blank cell) rather than dropping the child row. Compared as strings, because
|
|
606
|
+
* the identity value is a literal and the cell may be read through a path that
|
|
607
|
+
* ends at one too.
|
|
608
|
+
*/
|
|
609
|
+
function expandFieldClause(
|
|
610
|
+
field: PackExpandField,
|
|
611
|
+
entityUri: string,
|
|
612
|
+
lookup?: Pick<PackLookup, "by">,
|
|
613
|
+
): string {
|
|
614
|
+
const read = `?child ${formatTerm(field.property)} ?${field.name} .`;
|
|
615
|
+
if (!field.blankWhenSelf || !lookup) return ` OPTIONAL { ${read} }`;
|
|
616
|
+
const self = `?${RESERVED_VARIABLE_PREFIX}Self`;
|
|
617
|
+
return [
|
|
618
|
+
" OPTIONAL {",
|
|
619
|
+
` ${read}`,
|
|
620
|
+
` <${entityUri}> ${formatTerm(lookup.by)} ${self} .`,
|
|
621
|
+
` FILTER(STR(?${field.name}) != STR(${self}))`,
|
|
622
|
+
" }",
|
|
503
623
|
].join("\n");
|
|
504
624
|
}
|
|
505
625
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One rejected filter value, ONE list of the values it should have been.
|
|
3
|
+
*
|
|
4
|
+
* `variable list --symbol nope` printed the 40-name head of a 745-name
|
|
5
|
+
* vocabulary twice — once as the options line, once inside the recovery
|
|
6
|
+
* message, about 1.4KB of duplicate terminal output. The vocabulary rides
|
|
7
|
+
* `validOptions`, which every renderer already spells out, truncates and
|
|
8
|
+
* counts; a recovery says what to DO.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
type PragmaError,
|
|
14
|
+
renderErrorLlm,
|
|
15
|
+
renderErrorPlain,
|
|
16
|
+
} from "../../error/index.js";
|
|
17
|
+
import type { PackFilter } from "../types.js";
|
|
18
|
+
import { resolveFilterPredicates } from "./filterValues.js";
|
|
19
|
+
|
|
20
|
+
/** The 745-name catalogue that made the duplication measurable. */
|
|
21
|
+
const VOCABULARY = Array.from({ length: 745 }, (_, i) => `color.text.${i}`);
|
|
22
|
+
|
|
23
|
+
const FREE_FILTER: PackFilter = { param: "symbol", variable: "symbol" };
|
|
24
|
+
const SET_FILTER: PackFilter = {
|
|
25
|
+
param: "type",
|
|
26
|
+
variable: "type",
|
|
27
|
+
values: ["color", "dimension", "duration"],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The error a filter raises for a value it does not admit. */
|
|
31
|
+
function rejection(
|
|
32
|
+
filter: PackFilter,
|
|
33
|
+
value: string,
|
|
34
|
+
vocabulary?: readonly string[],
|
|
35
|
+
): PragmaError {
|
|
36
|
+
try {
|
|
37
|
+
resolveFilterPredicates(
|
|
38
|
+
[filter],
|
|
39
|
+
{ [filter.param]: value },
|
|
40
|
+
vocabulary ? new Map([[filter.param, vocabulary]]) : undefined,
|
|
41
|
+
"test:story",
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return error as PragmaError;
|
|
45
|
+
}
|
|
46
|
+
throw new Error("the filter admitted a value it should have refused");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How many times `needle` appears in `text`. */
|
|
50
|
+
const occurrences = (text: string, needle: string): number =>
|
|
51
|
+
text.split(needle).length - 1;
|
|
52
|
+
|
|
53
|
+
describe("a refused filter value names its vocabulary once", () => {
|
|
54
|
+
it("renders the catalogue once, not twice", () => {
|
|
55
|
+
const error = rejection(FREE_FILTER, "nope", VOCABULARY);
|
|
56
|
+
const plain = renderErrorPlain(error);
|
|
57
|
+
|
|
58
|
+
expect(occurrences(plain, "color.text.0,")).toBe(1);
|
|
59
|
+
expect(plain).toContain("Valid options (745), first 40:");
|
|
60
|
+
// The recovery says what to do, and names the flag — without the list.
|
|
61
|
+
expect(plain).toContain("Pick one of the 745 values --symbol accepts.");
|
|
62
|
+
expect(renderErrorLlm(error)).toContain("Pick one of the 745 values");
|
|
63
|
+
expect(occurrences(renderErrorLlm(error), "color.text.0,")).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("keeps the whole vocabulary on the machine channel", () => {
|
|
67
|
+
// Truncation is a RENDERING decision; a machine consumer loses nothing.
|
|
68
|
+
expect(rejection(FREE_FILTER, "nope", VOCABULARY).validOptions).toEqual(
|
|
69
|
+
[...VOCABULARY].sort(),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("says it once for a declared set too", () => {
|
|
74
|
+
const plain = renderErrorPlain(rejection(SET_FILTER, "nope"));
|
|
75
|
+
|
|
76
|
+
expect(occurrences(plain, "dimension")).toBe(1);
|
|
77
|
+
expect(plain).toContain("Valid options: color, dimension, duration");
|
|
78
|
+
expect(plain).toContain("Pick one of the 3 values --type accepts.");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("a short vocabulary stays cheap to read", () => {
|
|
82
|
+
const plain = renderErrorPlain(rejection(SET_FILTER, "nope"));
|
|
83
|
+
|
|
84
|
+
expect(plain.length).toBeLessThan(200);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admit (or refuse) the values a caller supplied for a list story's declared
|
|
3
|
+
* filters, and hand the admitted ones to the query builder.
|
|
4
|
+
*
|
|
5
|
+
* This is the half of filtering that does NOT move into the query. Deciding
|
|
6
|
+
* whether a value is admissible is a question about the graph's vocabulary;
|
|
7
|
+
* deciding which rows carry it is a question about the population. Keeping them
|
|
8
|
+
* apart is what makes the two answers a caller gets distinguishable, and that
|
|
9
|
+
* distinction is load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* - a value the vocabulary does NOT admit is `INVALID_INPUT`, carrying the
|
|
12
|
+
* admissible values as `validOptions` — the caller mistyped;
|
|
13
|
+
* - a value it DOES admit that no row carries is a calm empty list, exit 0,
|
|
14
|
+
* with the story's own `emptyRecovery` as the hint — `standard categories`
|
|
15
|
+
* reports a category with count 0, and asking for it is not a mistake.
|
|
16
|
+
*
|
|
17
|
+
* WHICH part of the graph is the vocabulary matters, and it is not the rows.
|
|
18
|
+
* Before the filters moved inside the query the rows were the FALLBACK
|
|
19
|
+
* vocabulary for a filter that declared none — knowingly narrower than the
|
|
20
|
+
* truth, but the only evidence there was. A page makes that fallback actively
|
|
21
|
+
* wrong: a real value outside the window would be refused as a bad argument,
|
|
22
|
+
* with a truncated list of alternatives. So the fallback is gone and the
|
|
23
|
+
* declaration rule replaces it — a filter declares `values` or a `vocabulary`
|
|
24
|
+
* query, and one that declares neither is refused where it is declared
|
|
25
|
+
* ({@link ../schema}) rather than misanswering at run time.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { PragmaError } from "../../error/index.js";
|
|
29
|
+
import type { PackFilter } from "../types.js";
|
|
30
|
+
import type { ListPredicate } from "./buildListQuery.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authoritative values per filter `param`, read from each filter's declared
|
|
34
|
+
* `vocabulary`. A filter with declared `values` needs no entry — the set IS the
|
|
35
|
+
* vocabulary.
|
|
36
|
+
*/
|
|
37
|
+
export type FilterVocabularies = ReadonlyMap<string, readonly string[]>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Turn the filter arguments a caller supplied into query predicates.
|
|
41
|
+
*
|
|
42
|
+
* @param filters - The story's declared filters (absent means none).
|
|
43
|
+
* @param params - Story parameters as provided by the surface.
|
|
44
|
+
* @param vocabularies - Authoritative values per value-free filter param.
|
|
45
|
+
* @param label - The story's label, for a configuration diagnosis.
|
|
46
|
+
* @returns One predicate per filter the caller supplied a value for.
|
|
47
|
+
* @throws PragmaError INVALID_INPUT when a value is not in a filter's declared
|
|
48
|
+
* set or its graph vocabulary, or when a value-free filter receives a
|
|
49
|
+
* non-string value; CONFIG_ERROR when a value-free filter declares no
|
|
50
|
+
* vocabulary.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveFilterPredicates(
|
|
53
|
+
filters: readonly PackFilter[] | undefined,
|
|
54
|
+
params: Record<string, unknown>,
|
|
55
|
+
vocabularies: FilterVocabularies | undefined,
|
|
56
|
+
label: string,
|
|
57
|
+
): ListPredicate[] {
|
|
58
|
+
const predicates: ListPredicate[] = [];
|
|
59
|
+
for (const filter of filters ?? []) {
|
|
60
|
+
const provided = params[filter.param];
|
|
61
|
+
if (provided === undefined) continue;
|
|
62
|
+
// A repeated CLI flag accumulates into an array; MCP args stay scalar.
|
|
63
|
+
// Several values for ONE filter are a union (a row matches any of them);
|
|
64
|
+
// several filters still combine conjunctively.
|
|
65
|
+
const occurrences = Array.isArray(provided) ? provided : [provided];
|
|
66
|
+
if (occurrences.length === 0) continue;
|
|
67
|
+
const values = filter.values;
|
|
68
|
+
const terms = occurrences.map((occurrence) =>
|
|
69
|
+
values === undefined
|
|
70
|
+
? requireStringValue(occurrence, filter)
|
|
71
|
+
: canonicalizeFilterValue(occurrence, filter, values),
|
|
72
|
+
);
|
|
73
|
+
if (values === undefined) {
|
|
74
|
+
rejectUnknownValue(
|
|
75
|
+
requireVocabulary(filter, vocabularies, label),
|
|
76
|
+
filter,
|
|
77
|
+
occurrences,
|
|
78
|
+
terms,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
predicates.push({
|
|
82
|
+
variable: filter.variable,
|
|
83
|
+
match: filter.match ?? "exact",
|
|
84
|
+
terms,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return predicates;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The vocabulary a value-free filter is checked against.
|
|
92
|
+
*
|
|
93
|
+
* @throws PragmaError CONFIG_ERROR when the filter declares neither `values`
|
|
94
|
+
* nor a `vocabulary` — the declaration rule the schema enforces, restated at
|
|
95
|
+
* the point of use so a story that reached here another way still says why.
|
|
96
|
+
*/
|
|
97
|
+
function requireVocabulary(
|
|
98
|
+
filter: PackFilter,
|
|
99
|
+
vocabularies: FilterVocabularies | undefined,
|
|
100
|
+
label: string,
|
|
101
|
+
): readonly string[] {
|
|
102
|
+
const vocabulary = vocabularies?.get(filter.param);
|
|
103
|
+
if (vocabulary) return vocabulary;
|
|
104
|
+
throw PragmaError.configError(
|
|
105
|
+
`Filter "--${filter.param}" in ${label} declares neither "values" nor a ` +
|
|
106
|
+
'"vocabulary" query, so there is nothing to check a caller\'s value against.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reject a value-free filter value the vocabulary does not admit, naming the
|
|
112
|
+
* ones it does.
|
|
113
|
+
*
|
|
114
|
+
* Silent when the vocabulary is EMPTY: an unbuilt or genuinely-empty store is
|
|
115
|
+
* not the caller's mistake, and the list's own `emptyRecovery` is the right
|
|
116
|
+
* voice for it.
|
|
117
|
+
*
|
|
118
|
+
* A value the vocabulary admits is never rejected here even when no row carries
|
|
119
|
+
* it — a real slug with zero standards is a calm empty list, which is what the
|
|
120
|
+
* tool descriptions document and what `standard categories` reporting count 0
|
|
121
|
+
* promises.
|
|
122
|
+
*
|
|
123
|
+
* @throws PragmaError INVALID_INPUT when a term matches none of the admissible
|
|
124
|
+
* values, carrying them as `validOptions`.
|
|
125
|
+
*/
|
|
126
|
+
function rejectUnknownValue(
|
|
127
|
+
vocabulary: readonly string[],
|
|
128
|
+
filter: PackFilter,
|
|
129
|
+
occurrences: readonly unknown[],
|
|
130
|
+
terms: readonly string[],
|
|
131
|
+
): void {
|
|
132
|
+
const admissible = new Map<string, string>();
|
|
133
|
+
for (const value of vocabulary) {
|
|
134
|
+
const key = value.normalize("NFC").toLowerCase();
|
|
135
|
+
if (!admissible.has(key)) admissible.set(key, value.normalize("NFC"));
|
|
136
|
+
}
|
|
137
|
+
if (admissible.size === 0) return;
|
|
138
|
+
const index = terms.findIndex((term) => !admissible.has(term.toLowerCase()));
|
|
139
|
+
if (index === -1) return;
|
|
140
|
+
const validOptions = [...admissible.values()].sort();
|
|
141
|
+
// The VALUES ride `validOptions`, which every renderer already prints (and
|
|
142
|
+
// truncates, and counts). Repeating them in the recovery printed the same
|
|
143
|
+
// 40-name, 1.4KB list twice on one error — see `error/validOptions.ts`.
|
|
144
|
+
throw PragmaError.invalidInput(filter.param, String(occurrences[index]), {
|
|
145
|
+
validOptions,
|
|
146
|
+
recovery: {
|
|
147
|
+
message: `Pick one of the ${validOptions.length} values --${filter.param} accepts.`,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @throws PragmaError INVALID_INPUT when a value-free filter value is not a string. */
|
|
153
|
+
function requireStringValue(provided: unknown, filter: PackFilter): string {
|
|
154
|
+
if (typeof provided !== "string") {
|
|
155
|
+
throw PragmaError.invalidInput(filter.param, String(provided), {
|
|
156
|
+
recovery: { message: `Provide a string value for --${filter.param}.` },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return provided.trim().normalize("NFC");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @throws PragmaError INVALID_INPUT when the value is not in the declared set. */
|
|
163
|
+
function canonicalizeFilterValue(
|
|
164
|
+
provided: unknown,
|
|
165
|
+
filter: PackFilter,
|
|
166
|
+
values: readonly string[],
|
|
167
|
+
): string {
|
|
168
|
+
if (typeof provided === "string") {
|
|
169
|
+
const normalized = provided.trim().normalize("NFC");
|
|
170
|
+
const match = values.find(
|
|
171
|
+
(value) =>
|
|
172
|
+
value.normalize("NFC").toLowerCase() === normalized.toLowerCase(),
|
|
173
|
+
);
|
|
174
|
+
if (match !== undefined) return match.normalize("NFC");
|
|
175
|
+
}
|
|
176
|
+
throw PragmaError.invalidInput(filter.param, String(provided), {
|
|
177
|
+
validOptions: [...values],
|
|
178
|
+
recovery: {
|
|
179
|
+
message: `Pick one of the ${values.length} values --${filter.param} accepts.`,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The SPARQL path — every generated read a pack story performs
|
|
3
|
-
* row operations that must NOT be generated.
|
|
2
|
+
* The SPARQL path — every generated read a pack story performs.
|
|
4
3
|
*
|
|
5
4
|
* The domain is organised around one invariant: user input never reaches query
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* goes through, which is what
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
5
|
+
* TEXT. The query builders compose only from validated pack terms, the page's
|
|
6
|
+
* own integers, and escaped literals bound through `VALUES` — so a name a user
|
|
7
|
+
* typed becomes a SPARQL string literal or nothing; `runSelect` is the single
|
|
8
|
+
* choke point every one of those trusted queries goes through, which is what
|
|
9
|
+
* makes "a generated query failed" distinguishable from "the user's own
|
|
10
|
+
* `graph query` failed".
|
|
11
|
+
*
|
|
12
|
+
* Filtering and search used to sit here as predicates over the resolved ROWS —
|
|
13
|
+
* kept in this domain precisely because they were the operations it would
|
|
14
|
+
* otherwise be tempting to express as query text. They are query text now, and
|
|
15
|
+
* the temptation is answered a different way: {@link ./buildListQuery} is the
|
|
16
|
+
* only thing that composes a predicate, it binds every caller-supplied value,
|
|
17
|
+
* and it never edits the author's own query. What stayed on the row side is the
|
|
18
|
+
* part that was never about rows at all — deciding whether a value is
|
|
19
|
+
* ADMISSIBLE ({@link ./filterValues}), which is a question about the graph's
|
|
20
|
+
* vocabulary and is what keeps a refusal distinguishable from a calm empty
|
|
21
|
+
* answer.
|
|
15
22
|
*
|
|
16
23
|
* The escaping primitives stay internal. They are how the builders keep that
|
|
17
24
|
* invariant, not a service for composing query text elsewhere — a caller with
|
|
18
25
|
* the escaper is a caller who can build an unvalidated query, which is exactly
|
|
19
|
-
* the shape this domain exists to prevent.
|
|
26
|
+
* the shape this domain exists to prevent. {@link ./projection}'s reader is
|
|
27
|
+
* internal for the same reason: it exists so a wrapping select can reproduce
|
|
28
|
+
* the author's own projection, not so callers can reason about author text.
|
|
20
29
|
*
|
|
21
30
|
* Two names re-exported by `buildLookupQuery.ts` for its own callers'
|
|
22
31
|
* convenience — the disclosure-level field and expand selectors — are also
|
|
@@ -25,8 +34,12 @@
|
|
|
25
34
|
* pass-through that has no reason to be permanent.
|
|
26
35
|
*/
|
|
27
36
|
|
|
28
|
-
export {
|
|
29
|
-
|
|
37
|
+
export type {
|
|
38
|
+
ListPredicate,
|
|
39
|
+
ListSearch,
|
|
40
|
+
ListWindow,
|
|
41
|
+
} from "./buildListQuery.js";
|
|
42
|
+
export { buildListQuery } from "./buildListQuery.js";
|
|
30
43
|
export {
|
|
31
44
|
buildExpandQuery,
|
|
32
45
|
buildLookupByIriQuery,
|
|
@@ -34,4 +47,7 @@ export {
|
|
|
34
47
|
buildLookupQuery,
|
|
35
48
|
buildNameResolveQuery,
|
|
36
49
|
} from "./buildLookupQuery.js";
|
|
50
|
+
export type { FilterVocabularies } from "./filterValues.js";
|
|
51
|
+
export { resolveFilterPredicates } from "./filterValues.js";
|
|
37
52
|
export { runSelect } from "./runSelect.js";
|
|
53
|
+
export { readSearchTerm } from "./searchTerm.js";
|
|
@@ -172,3 +172,66 @@ describe("runSelect — an unbound prefix is diagnosed by PROVENANCE", () => {
|
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
|
+
|
|
176
|
+
describe("runSelect — one SECTION degrades where the answer itself raises", () => {
|
|
177
|
+
const unbound = (): Error => new Error("Prefix not found: anatomy");
|
|
178
|
+
|
|
179
|
+
it("answers no rows for a section whose vocabulary the store does not bind", async () => {
|
|
180
|
+
// A lookup's section can name a vocabulary from a different pack than the
|
|
181
|
+
// entity's: a block is `ds:`, and the style key and state of the tokens it
|
|
182
|
+
// consumes are the anatomy DSL's. Taking the whole entity down over one
|
|
183
|
+
// absent section would fail `block lookup` outright on a graph that can
|
|
184
|
+
// answer every other section — so the section renders empty, which is what
|
|
185
|
+
// the lookup's GraphQL lane already does with the same condition.
|
|
186
|
+
//
|
|
187
|
+
// It cannot hide an unbuilt store: a section is only read after the base
|
|
188
|
+
// resolve has already answered from it.
|
|
189
|
+
const rows = await runSelect(
|
|
190
|
+
throwingRuntime(unbound()),
|
|
191
|
+
"SELECT ?key WHERE { ?child anatomy:styleKey ?key }",
|
|
192
|
+
distributionSource("pragma.conf.ts"),
|
|
193
|
+
{ degradeOnUnboundPrefix: true },
|
|
194
|
+
);
|
|
195
|
+
expect(rows).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("degrades for a package and config story too, by the same rule", async () => {
|
|
199
|
+
// The provenance split above is about WHICH error an unbound prefix is. A
|
|
200
|
+
// section that degrades raises none, so there is nothing left to diagnose
|
|
201
|
+
// — and a third party's section is no more entitled to take an entity down
|
|
202
|
+
// than the distribution's.
|
|
203
|
+
for (const source of [PACKAGE, CONFIG]) {
|
|
204
|
+
expect(
|
|
205
|
+
await runSelect(
|
|
206
|
+
throwingRuntime(unbound()),
|
|
207
|
+
"SELECT ?x WHERE { ?x a acme:Thing }",
|
|
208
|
+
source,
|
|
209
|
+
{ degradeOnUnboundPrefix: true },
|
|
210
|
+
),
|
|
211
|
+
).toEqual([]);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("still passes a non-prefix failure through, and still refuses a non-SELECT", async () => {
|
|
216
|
+
// Narrow by construction: the ONE condition it swallows is the one that
|
|
217
|
+
// says "this pack's vocabulary is narrower than the story's". Everything
|
|
218
|
+
// else is a bug and still raises.
|
|
219
|
+
const boom = new Error("some other engine failure");
|
|
220
|
+
await expect(
|
|
221
|
+
runSelect(throwingRuntime(boom), "SELECT ?x WHERE {}", PACKAGE, {
|
|
222
|
+
degradeOnUnboundPrefix: true,
|
|
223
|
+
}),
|
|
224
|
+
).rejects.toBe(boom);
|
|
225
|
+
|
|
226
|
+
const askRuntime = {
|
|
227
|
+
query: {
|
|
228
|
+
sparql: async () => ({ type: "ask", boolean: true }),
|
|
229
|
+
} as unknown as PragmaRuntime["query"],
|
|
230
|
+
};
|
|
231
|
+
await expect(
|
|
232
|
+
runSelect(askRuntime, "ASK { ?x ?y ?z }", PACKAGE, {
|
|
233
|
+
degradeOnUnboundPrefix: true,
|
|
234
|
+
}),
|
|
235
|
+
).rejects.toMatchObject({ code: "CONFIG_ERROR" });
|
|
236
|
+
});
|
|
237
|
+
});
|