@canonical/pragma-cli 0.38.0 → 0.40.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 +1 -1
- package/dist/package.json +14 -14
- package/dist/pragma.conf.js +273 -176
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/capabilities.verb.js +2 -0
- package/dist/src/capabilities/capabilities/capabilities.verb.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +47 -51
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/index.js +2 -0
- package/dist/src/capabilities/capabilities/index.js.map +1 -1
- package/dist/src/capabilities/capabilities/types.js +5 -5
- package/dist/src/capabilities/colophon/colophon.render.js +7 -4
- package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
- package/dist/src/capabilities/colophon/colophon.verb.js +1 -0
- package/dist/src/capabilities/colophon/colophon.verb.js.map +1 -1
- package/dist/src/capabilities/config/get.verb.js +2 -0
- package/dist/src/capabilities/config/get.verb.js.map +1 -1
- package/dist/src/capabilities/config/runField.js +2 -2
- package/dist/src/capabilities/config/runField.js.map +1 -1
- package/dist/src/capabilities/config/set.verb.js +2 -0
- package/dist/src/capabilities/config/set.verb.js.map +1 -1
- package/dist/src/capabilities/config/show.verb.js +1 -0
- package/dist/src/capabilities/config/show.verb.js.map +1 -1
- package/dist/src/capabilities/config/unset.verb.js +2 -0
- package/dist/src/capabilities/config/unset.verb.js.map +1 -1
- package/dist/src/capabilities/create/constants.js +2 -2
- package/dist/src/capabilities/create/create.verb.js +14 -4
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/distribution.js +3 -1
- package/dist/src/capabilities/distribution.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +10 -2
- package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
- package/dist/src/capabilities/doctor/doctor.verb.js +2 -0
- package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
- package/dist/src/capabilities/graph/connect.verb.js +2 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -1
- package/dist/src/capabilities/graph/inspect.render.js +7 -1
- package/dist/src/capabilities/graph/inspect.render.js.map +1 -1
- package/dist/src/capabilities/graph/inspect.verb.js +2 -0
- package/dist/src/capabilities/graph/inspect.verb.js.map +1 -1
- package/dist/src/capabilities/graph/query.render.js +37 -5
- package/dist/src/capabilities/graph/query.render.js.map +1 -1
- package/dist/src/capabilities/graph/query.verb.js +2 -0
- package/dist/src/capabilities/graph/query.verb.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +28 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -1
- package/dist/src/capabilities/graph/runConnect.js +3 -3
- package/dist/src/capabilities/graph/runConnect.js.map +1 -1
- package/dist/src/capabilities/graph/runQuery.js +8 -6
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/info/info.verb.js +2 -0
- package/dist/src/capabilities/info/info.verb.js.map +1 -1
- package/dist/src/capabilities/meta/mcp.verb.js +1 -0
- package/dist/src/capabilities/meta/mcp.verb.js.map +1 -1
- package/dist/src/capabilities/ontology/verbs.js +7 -10
- package/dist/src/capabilities/ontology/verbs.js.map +1 -1
- package/dist/src/capabilities/prompt/verbs.js +5 -4
- package/dist/src/capabilities/prompt/verbs.js.map +1 -1
- package/dist/src/capabilities/resources/provider.js +6 -3
- package/dist/src/capabilities/resources/provider.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +13 -5
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/shared/calls.js +16 -0
- package/dist/src/capabilities/shared/calls.js.map +1 -0
- package/dist/src/capabilities/skill/render.js +7 -5
- package/dist/src/capabilities/skill/render.js.map +1 -1
- package/dist/src/capabilities/skill/verbs.js +5 -4
- package/dist/src/capabilities/skill/verbs.js.map +1 -1
- package/dist/src/capabilities/sources/collectStatus.js +6 -1
- package/dist/src/capabilities/sources/collectStatus.js.map +1 -1
- package/dist/src/capabilities/sources/index.js +4 -3
- package/dist/src/capabilities/sources/index.js.map +1 -1
- package/dist/src/capabilities/sources/reset.render.js +37 -0
- package/dist/src/capabilities/sources/reset.render.js.map +1 -0
- package/dist/src/capabilities/sources/reset.verb.js +51 -0
- package/dist/src/capabilities/sources/reset.verb.js.map +1 -0
- package/dist/src/capabilities/sources/runReset.js +59 -0
- package/dist/src/capabilities/sources/runReset.js.map +1 -0
- package/dist/src/capabilities/sources/runUpdate.js +7 -6
- package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
- package/dist/src/capabilities/sources/status.render.js +24 -8
- package/dist/src/capabilities/sources/status.render.js.map +1 -1
- package/dist/src/capabilities/sources/status.verb.js +1 -0
- package/dist/src/capabilities/sources/status.verb.js.map +1 -1
- package/dist/src/capabilities/sources/update.verb.js +2 -0
- package/dist/src/capabilities/sources/update.verb.js.map +1 -1
- package/dist/src/capabilities/upgrade/upgrade.verb.js +1 -0
- package/dist/src/capabilities/upgrade/upgrade.verb.js.map +1 -1
- package/dist/src/capabilities/version/version.verb.js +1 -0
- package/dist/src/capabilities/version/version.verb.js.map +1 -1
- package/dist/src/constants.js +4 -3
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/index.js +1 -1
- package/dist/src/kernel/error/index.js.map +1 -1
- package/dist/src/kernel/error/recovery.js +24 -42
- package/dist/src/kernel/error/recovery.js.map +1 -1
- package/dist/src/kernel/packs/collect.js +123 -9
- package/dist/src/kernel/packs/collect.js.map +1 -1
- package/dist/src/kernel/packs/compile.js +74 -15
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js +3 -3
- package/dist/src/kernel/packs/graphql/fetchGraphqlLookup.js.map +1 -1
- package/dist/src/kernel/packs/iri.js +2 -4
- package/dist/src/kernel/packs/iri.js.map +1 -1
- package/dist/src/kernel/packs/renderPack.js +90 -50
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +202 -90
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +89 -30
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +66 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildListQuery.js +80 -13
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +41 -13
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +32 -23
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +3 -5
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/storyRules.js +31 -4
- package/dist/src/kernel/packs/storyRules.js.map +1 -1
- package/dist/src/kernel/packs/tierScope.js +3 -3
- package/dist/src/kernel/packs/tierScope.js.map +1 -1
- package/dist/src/kernel/packs/types.js +4 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +4 -0
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +32 -8
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +44 -1
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/cli/verbHelp.js +17 -18
- package/dist/src/kernel/project/cli/verbHelp.js.map +1 -1
- package/dist/src/kernel/project/mcp/buildServer.js +11 -4
- package/dist/src/kernel/project/mcp/buildServer.js.map +1 -1
- package/dist/src/kernel/project/mcp/envelope.js +3 -2
- package/dist/src/kernel/project/mcp/envelope.js.map +1 -1
- package/dist/src/kernel/project/mcp/instructions.js +101 -31
- package/dist/src/kernel/project/mcp/instructions.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +59 -23
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +3 -3
- package/dist/src/kernel/render/turtle.js +21 -4
- package/dist/src/kernel/render/turtle.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/build.js +54 -1
- package/dist/src/kernel/runtime/graphpack/build.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/manifest.js +14 -0
- package/dist/src/kernel/runtime/graphpack/manifest.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +3 -5
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/schemas.js +1 -0
- package/dist/src/kernel/runtime/graphpack/schemas.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +8 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/packVersion.js +75 -0
- package/dist/src/kernel/runtime/packVersion.js.map +1 -0
- package/dist/src/kernel/runtime/readEntity.js +43 -4
- package/dist/src/kernel/runtime/readEntity.js.map +1 -1
- package/dist/src/kernel/runtime/resolveSources.js +61 -9
- package/dist/src/kernel/runtime/resolveSources.js.map +1 -1
- package/dist/src/kernel/runtime/storeReadiness.js +3 -2
- package/dist/src/kernel/runtime/storeReadiness.js.map +1 -1
- package/dist/src/kernel/spec/call.js +167 -0
- package/dist/src/kernel/spec/call.js.map +1 -0
- package/dist/src/kernel/spec/emitReference.js +9 -13
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/dist/src/kernel/spec/emitSurface.js +8 -1
- package/dist/src/kernel/spec/emitSurface.js.map +1 -1
- package/dist/src/kernel/spec/guidance.js +63 -0
- package/dist/src/kernel/spec/guidance.js.map +1 -0
- package/dist/src/kernel/spec/wireType.js +36 -0
- package/dist/src/kernel/spec/wireType.js.map +1 -0
- package/package.json +14 -14
- package/pragma.conf.ts +294 -177
- package/src/capabilities/block.parity.test.ts +1 -1
- package/src/capabilities/callRule.test.ts +147 -0
- package/src/capabilities/capabilities/capabilities.test.ts +47 -19
- package/src/capabilities/capabilities/capabilities.verb.ts +2 -0
- package/src/capabilities/capabilities/catalog.ts +60 -55
- package/src/capabilities/capabilities/index.ts +2 -0
- package/src/capabilities/capabilities/types.ts +16 -19
- package/src/capabilities/colophon/colophon.render.ts +9 -5
- package/src/capabilities/colophon/colophon.verb.ts +1 -0
- package/src/capabilities/config/get.verb.ts +2 -0
- package/src/capabilities/config/runField.ts +3 -3
- package/src/capabilities/config/set.verb.ts +3 -0
- package/src/capabilities/config/show.verb.ts +2 -0
- package/src/capabilities/config/unset.verb.ts +2 -0
- package/src/capabilities/create/constants.ts +2 -2
- package/src/capabilities/create/create.verb.ts +16 -0
- package/src/capabilities/create/createGrammar.test.ts +1 -1
- package/src/capabilities/distribution.test.ts +6 -3
- package/src/capabilities/distribution.ts +6 -0
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +14 -2
- package/src/capabilities/doctor/doctor.test.ts +38 -1
- package/src/capabilities/doctor/doctor.verb.ts +2 -0
- package/src/capabilities/emptyRecovery.test.ts +5 -0
- package/src/capabilities/graph/connect.verb.ts +2 -0
- package/src/capabilities/graph/inspect.render.ts +9 -1
- package/src/capabilities/graph/inspect.test.ts +73 -0
- package/src/capabilities/graph/inspect.verb.ts +2 -0
- package/src/capabilities/graph/query.render.test.ts +35 -0
- package/src/capabilities/graph/query.render.ts +45 -7
- package/src/capabilities/graph/query.shipped.exec.test.ts +44 -0
- package/src/capabilities/graph/query.verb.ts +2 -0
- package/src/capabilities/graph/queryText.test.ts +55 -1
- package/src/capabilities/graph/queryText.ts +47 -0
- package/src/capabilities/graph/runConnect.ts +4 -5
- package/src/capabilities/graph/runQuery.ts +23 -9
- package/src/capabilities/info/info.verb.ts +2 -0
- package/src/capabilities/links.shipped.exec.test.ts +398 -0
- package/src/capabilities/listBudget.shipped.exec.test.ts +2 -2
- package/src/capabilities/listLookup.shipped.exec.test.ts +122 -13
- package/src/capabilities/listQuery.shipped.exec.test.ts +39 -16
- package/src/capabilities/lookupBudget.shipped.exec.test.ts +208 -0
- package/src/capabilities/meta/mcp.verb.ts +2 -0
- package/src/capabilities/ontology/ontology.test.ts +24 -0
- package/src/capabilities/ontology/verbs.ts +16 -12
- package/src/capabilities/prompt/verbs.ts +10 -4
- package/src/capabilities/resources/provider.ts +7 -3
- package/src/capabilities/resources/resources.test.ts +2 -1
- package/src/capabilities/setup/setup.verb.ts +16 -0
- package/src/capabilities/shared/calls.ts +20 -0
- package/src/capabilities/skill/render.ts +9 -6
- package/src/capabilities/skill/verbs.ts +8 -4
- package/src/capabilities/sources/collectStatus.ts +6 -1
- package/src/capabilities/sources/index.ts +4 -3
- package/src/capabilities/sources/reset.render.test.ts +33 -0
- package/src/capabilities/sources/reset.render.ts +43 -0
- package/src/capabilities/sources/reset.verb.ts +59 -0
- package/src/capabilities/sources/runReset.ts +65 -0
- package/src/capabilities/sources/runUpdate.ts +12 -11
- package/src/capabilities/sources/sources.test.ts +158 -3
- package/src/capabilities/sources/status.render.test.ts +63 -1
- package/src/capabilities/sources/status.render.ts +25 -8
- package/src/capabilities/sources/status.verb.ts +2 -0
- package/src/capabilities/sources/types.ts +26 -0
- package/src/capabilities/sources/update.verb.ts +3 -0
- package/src/capabilities/surface.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +3 -2
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +14 -2
- package/src/capabilities/upgrade/upgrade.verb.ts +1 -0
- package/src/capabilities/variable.parity.test.ts +2 -1
- package/src/capabilities/version/version.verb.ts +1 -0
- package/src/capabilities/wireContract.test.ts +110 -0
- package/src/constants.ts +4 -3
- package/src/kernel/copy.test.ts +1 -1
- package/src/kernel/error/PragmaError.test.ts +7 -3
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +52 -18
- package/src/kernel/error/recovery.ts +25 -46
- package/src/kernel/error/types.ts +1 -1
- package/src/kernel/packs/collect.test.ts +79 -0
- package/src/kernel/packs/collect.ts +151 -11
- package/src/kernel/packs/compile.test.ts +107 -10
- package/src/kernel/packs/compile.ts +100 -11
- package/src/kernel/packs/filteredEmpty.test.ts +22 -5
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +5 -6
- package/src/kernel/packs/graphqlSource.test.ts +42 -0
- package/src/kernel/packs/iri.ts +5 -4
- package/src/kernel/packs/listNotice.test.ts +1 -1
- package/src/kernel/packs/nounLink.test.ts +385 -0
- package/src/kernel/packs/renderPack.ts +137 -57
- package/src/kernel/packs/resolveEntity.test.ts +170 -1
- package/src/kernel/packs/resolveEntity.ts +281 -101
- package/src/kernel/packs/runBodies.ts +133 -31
- package/src/kernel/packs/schema.ts +77 -12
- package/src/kernel/packs/sparql/buildListQuery.test.ts +78 -1
- package/src/kernel/packs/sparql/buildListQuery.ts +106 -16
- package/src/kernel/packs/sparql/buildLookupQuery.ts +44 -13
- package/src/kernel/packs/sparql/filterValues.test.ts +36 -4
- package/src/kernel/packs/sparql/filterValues.ts +50 -31
- package/src/kernel/packs/sparql/runSelect.ts +4 -6
- package/src/kernel/packs/storyRules.ts +37 -4
- package/src/kernel/packs/tierScope.test.ts +120 -6
- package/src/kernel/packs/tierScope.ts +5 -7
- package/src/kernel/packs/toolDescription.test.ts +59 -74
- package/src/kernel/packs/types.ts +93 -11
- package/src/kernel/project/cli/buildProgram.ts +4 -0
- package/src/kernel/project/cli/dispatch.ts +34 -8
- package/src/kernel/project/cli/suggest.test.ts +77 -0
- package/src/kernel/project/cli/suggestNames.ts +47 -1
- package/src/kernel/project/cli/verbHelp.ts +16 -19
- package/src/kernel/project/mcp/buildServer.ts +15 -4
- package/src/kernel/project/mcp/envelope.ts +3 -2
- package/src/kernel/project/mcp/instructions.ts +123 -35
- package/src/kernel/project/mcp/prompts/prompts.test.ts +136 -20
- package/src/kernel/project/mcp/registerVerb.test.ts +28 -0
- package/src/kernel/project/mcp/registerVerb.ts +67 -29
- package/src/kernel/render/__snapshots__/renderers.test.ts.snap +1 -1
- package/src/kernel/render/renderers.test.ts +2 -2
- package/src/kernel/render/renderers.ts +3 -3
- package/src/kernel/render/turtle.ts +24 -4
- package/src/kernel/runtime/graphpack/build.ts +58 -1
- package/src/kernel/runtime/graphpack/graphpack.test.ts +20 -0
- package/src/kernel/runtime/graphpack/manifest.ts +15 -0
- package/src/kernel/runtime/graphpack/read.ts +4 -6
- package/src/kernel/runtime/graphpack/schemas.ts +1 -0
- package/src/kernel/runtime/graphpack/types.ts +13 -0
- package/src/kernel/runtime/loadSession.ts +8 -1
- package/src/kernel/runtime/packVersion.test.ts +61 -0
- package/src/kernel/runtime/packVersion.ts +84 -0
- package/src/kernel/runtime/readEntity.ts +61 -6
- package/src/kernel/runtime/resolveSources.test.ts +90 -7
- package/src/kernel/runtime/resolveSources.ts +91 -10
- package/src/kernel/runtime/storeReadiness.ts +4 -4
- package/src/kernel/runtime/types.ts +12 -0
- package/src/kernel/spec/call.test.ts +95 -0
- package/src/kernel/spec/call.ts +186 -0
- package/src/kernel/spec/emitReference.ts +9 -13
- package/src/kernel/spec/emitSurface.ts +9 -1
- package/src/kernel/spec/guidance.test.ts +49 -0
- package/src/kernel/spec/guidance.ts +68 -0
- package/src/kernel/spec/index.ts +4 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +64 -7
- package/src/kernel/spec/wireType.ts +53 -0
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +2 -1
- package/src/testing/behavioral/journeys.firstInstall.test.ts +72 -2
- package/src/testing/behavioral/parity.test.ts +40 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +1 -1
- package/src/testing/eval/cases/assembly.ts +4 -3
- package/src/testing/eval/cases/stable.ts +2 -3
- package/src/testing/helpers/callSchema.ts +40 -0
- package/src/testing/helpers/parity.ts +35 -2
- package/src/testing/setupCallChecking.ts +30 -0
- package/dist/src/capabilities/capabilities/hints.js +0 -223
- package/dist/src/capabilities/capabilities/hints.js.map +0 -1
- package/src/capabilities/capabilities/hints.ts +0 -266
|
@@ -453,10 +453,12 @@ function createVerb(
|
|
|
453
453
|
summary: string,
|
|
454
454
|
params: ParamSpec[],
|
|
455
455
|
examples: VerbSpec["examples"],
|
|
456
|
+
guidance: Required<Pick<VerbSpec, "useWhen" | "example">>,
|
|
456
457
|
): VerbSpec<Record<string, unknown>, GeneratorResult> {
|
|
457
458
|
return {
|
|
458
459
|
path: ["create", kind],
|
|
459
460
|
summary,
|
|
461
|
+
...guidance,
|
|
460
462
|
params,
|
|
461
463
|
output: CREATE_OUTPUT,
|
|
462
464
|
examples,
|
|
@@ -507,6 +509,11 @@ export const createVerbs: Record<
|
|
|
507
509
|
note: "preview the files without writing",
|
|
508
510
|
},
|
|
509
511
|
],
|
|
512
|
+
{
|
|
513
|
+
useWhen:
|
|
514
|
+
"when asked to start a new React, Svelte or Lit component with its tests, stories and styles",
|
|
515
|
+
example: { framework: "react", componentPath: "src/components/Button" },
|
|
516
|
+
},
|
|
510
517
|
),
|
|
511
518
|
package: createVerb(
|
|
512
519
|
"package",
|
|
@@ -520,6 +527,10 @@ export const createVerbs: Record<
|
|
|
520
527
|
cmd: `${BIN_NAME} create package --name @canonical/my-tool --no-run-install`,
|
|
521
528
|
},
|
|
522
529
|
],
|
|
530
|
+
{
|
|
531
|
+
useWhen: "when asked to start a new npm package inside the monorepo",
|
|
532
|
+
example: { name: "@canonical/my-lib", type: "library" },
|
|
533
|
+
},
|
|
523
534
|
),
|
|
524
535
|
application: createVerb(
|
|
525
536
|
"application",
|
|
@@ -530,5 +541,10 @@ export const createVerbs: Record<
|
|
|
530
541
|
{ cmd: `${BIN_NAME} create application react my-app --relay` },
|
|
531
542
|
{ cmd: `${BIN_NAME} create application react my-app --rendering spa` },
|
|
532
543
|
],
|
|
544
|
+
{
|
|
545
|
+
useWhen:
|
|
546
|
+
"when asked to start a new React application, server-rendered or client-only",
|
|
547
|
+
example: { appPath: "my-app" },
|
|
548
|
+
},
|
|
533
549
|
),
|
|
534
550
|
};
|
|
@@ -286,7 +286,7 @@ describe("the mounted create grammar (subprocess)", () => {
|
|
|
286
286
|
expect(matchedEnvelope.error.suggestions).toBeUndefined();
|
|
287
287
|
// D5's third route, pinned as the INVARIANT and not only the output:
|
|
288
288
|
// this cli is COMPUTED at the mount (chain[0] = the root program's
|
|
289
|
-
// name), bypassing
|
|
289
|
+
// name), bypassing callRecovery and invisible to copy.test.ts's
|
|
290
290
|
// quoted-literal position rule — so the expectation is COMPOSED from
|
|
291
291
|
// the same constant the derivation must land on. A `startsWith` check
|
|
292
292
|
// behind an exact literal could never fail (the literal throws first
|
|
@@ -63,7 +63,7 @@ describe("the distribution's declared stories (PROTECTED)", () => {
|
|
|
63
63
|
},
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
-
it('every declared filter names "values" or a "
|
|
66
|
+
it('every declared filter names "values", a "vocabulary" or a "noun"', () => {
|
|
67
67
|
// The rule the round-trip above already enforces, said OUT LOUD. It was
|
|
68
68
|
// only implied there — a story satisfies the whole grammar or it does not,
|
|
69
69
|
// and a reader looking for this particular property found no assertion
|
|
@@ -87,6 +87,7 @@ describe("the distribution's declared stories (PROTECTED)", () => {
|
|
|
87
87
|
"implementation list --library",
|
|
88
88
|
"implementation list --platform",
|
|
89
89
|
"standard list --category",
|
|
90
|
+
"token consumers --block",
|
|
90
91
|
"token consumers --key",
|
|
91
92
|
"token consumers --state",
|
|
92
93
|
"token consumers --symbol",
|
|
@@ -105,8 +106,10 @@ describe("the distribution's declared stories (PROTECTED)", () => {
|
|
|
105
106
|
]);
|
|
106
107
|
for (const [label, filter] of filters) {
|
|
107
108
|
expect(
|
|
108
|
-
filter.values !== undefined ||
|
|
109
|
-
|
|
109
|
+
filter.values !== undefined ||
|
|
110
|
+
filter.vocabulary !== undefined ||
|
|
111
|
+
filter.noun !== undefined,
|
|
112
|
+
`${label} declares no "values", "vocabulary" or "noun"`,
|
|
110
113
|
).toBe(true);
|
|
111
114
|
// And never both — a declared set IS the vocabulary.
|
|
112
115
|
expect(
|
|
@@ -22,6 +22,7 @@ import conf from "../../pragma.conf.js";
|
|
|
22
22
|
import { compileStoryModule } from "../kernel/packs/compile.js";
|
|
23
23
|
import {
|
|
24
24
|
distributionSource,
|
|
25
|
+
type NounLookups,
|
|
25
26
|
type PackDefinition,
|
|
26
27
|
} from "../kernel/packs/types.js";
|
|
27
28
|
import { DEFAULT_PREFIX_MAP } from "../kernel/render/index.js";
|
|
@@ -34,6 +35,10 @@ export const declaredStories: ReadonlyMap<string, PackDefinition> = new Map(
|
|
|
34
35
|
.map((story): readonly [string, PackDefinition] => [story.noun, story]),
|
|
35
36
|
);
|
|
36
37
|
|
|
38
|
+
/** The lookup each declared noun resolves names through, for a filter naming it. */
|
|
39
|
+
export const declaredLookups: NounLookups = (noun) =>
|
|
40
|
+
declaredStories.get(noun)?.lookup;
|
|
41
|
+
|
|
37
42
|
/**
|
|
38
43
|
* Those stories compiled to capability modules, by noun.
|
|
39
44
|
*
|
|
@@ -49,6 +54,7 @@ export const storyModules: ReadonlyMap<string, CapabilityModule> = new Map(
|
|
|
49
54
|
story,
|
|
50
55
|
distributionSource("pragma.conf.ts"),
|
|
51
56
|
DEFAULT_PREFIX_MAP,
|
|
57
|
+
declaredLookups,
|
|
52
58
|
),
|
|
53
59
|
]),
|
|
54
60
|
);
|
|
@@ -28,7 +28,10 @@ import {
|
|
|
28
28
|
import { activeStories } from "../../../kernel/runtime/graphpack/stories.js";
|
|
29
29
|
import type { PragmaRuntime } from "../../../kernel/runtime/index.js";
|
|
30
30
|
import type { SourcesDecision } from "../../../kernel/runtime/resolveSources.js";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
describeIgnoredPack,
|
|
33
|
+
resolveSources,
|
|
34
|
+
} from "../../../kernel/runtime/resolveSources.js";
|
|
32
35
|
import type { CheckItem, CheckResult } from "../types.js";
|
|
33
36
|
|
|
34
37
|
/**
|
|
@@ -148,6 +151,15 @@ export async function checkPackageRefs(
|
|
|
148
151
|
const composition = `${packs.length} ${packs.length === 1 ? "pack" : "packs"}, ${entities} entities${suffix}`;
|
|
149
152
|
|
|
150
153
|
if (decision.kind === "embedded") {
|
|
154
|
+
// A pack this project built, passed over because an older CLI built it. The
|
|
155
|
+
// check still PASSES — the snapshot is answering and its answers are the
|
|
156
|
+
// current ones — so the account of the pack goes in the detail, next to the
|
|
157
|
+
// account of what IS answering, and the remedy stays unset: there is
|
|
158
|
+
// nothing to fix, only two ways to change it, and the sentence names both.
|
|
159
|
+
const ignored =
|
|
160
|
+
decision.ignoredPack === undefined
|
|
161
|
+
? ""
|
|
162
|
+
: ` · ${describeIgnoredPack(decision.ignoredPack)}`;
|
|
151
163
|
return {
|
|
152
164
|
name,
|
|
153
165
|
status: "pass",
|
|
@@ -157,7 +169,7 @@ export async function checkPackageRefs(
|
|
|
157
169
|
// neither. "shipped with the CLI" rather than "embedded snapshot" — the
|
|
158
170
|
// reader needs to know where these packs came from, not what the build
|
|
159
171
|
// calls the artefact.
|
|
160
|
-
detail: `shipped with the CLI — ${composition} · run \`${BIN_NAME} sources update\` to build from your own configured packs instead`,
|
|
172
|
+
detail: `shipped with the CLI — ${composition} · run \`${BIN_NAME} sources update\` to build from your own configured packs instead${ignored}`,
|
|
161
173
|
...(items.length > 0 ? { items } : {}),
|
|
162
174
|
};
|
|
163
175
|
}
|
|
@@ -16,12 +16,13 @@ import {
|
|
|
16
16
|
writeFileSync,
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
20
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
21
21
|
import type { ConfigLayers } from "../../kernel/config/types.js";
|
|
22
22
|
import { executeVerb } from "../../kernel/project/cli/dispatch.js";
|
|
23
23
|
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
24
24
|
import { createQueryFacade } from "../../kernel/runtime/facade.js";
|
|
25
|
+
import { activePackPath, packDir } from "../../kernel/runtime/paths.js";
|
|
25
26
|
import type {
|
|
26
27
|
GlobalFlags,
|
|
27
28
|
LazyStore,
|
|
@@ -212,6 +213,42 @@ describe("doctor — the pack-refs check", () => {
|
|
|
212
213
|
}
|
|
213
214
|
});
|
|
214
215
|
|
|
216
|
+
it("still passes when a pack an older CLI built is passed over — and names it", async () => {
|
|
217
|
+
// The upgrade state. The check PASSES: the snapshot is answering and its
|
|
218
|
+
// answers are the current ones, so there is nothing to fix — only a pack to
|
|
219
|
+
// account for, and two ways to change it. Hence a detail, not a remedy.
|
|
220
|
+
const cwd = tmp("pragma-proj-");
|
|
221
|
+
const hash = "b".repeat(64);
|
|
222
|
+
const dir = packDir(hash);
|
|
223
|
+
mkdirSync(dir, { recursive: true });
|
|
224
|
+
writeFileSync(join(dir, "data.nq"), "<urn:s> <urn:p> <urn:o> .\n");
|
|
225
|
+
writeFileSync(join(dir, "schema.json"), "{}");
|
|
226
|
+
writeFileSync(join(dir, "index.json"), "{}");
|
|
227
|
+
writeFileSync(join(dir, "stories.json"), "[]");
|
|
228
|
+
writeFileSync(
|
|
229
|
+
join(dir, "manifest.json"),
|
|
230
|
+
JSON.stringify({
|
|
231
|
+
name: "pragma",
|
|
232
|
+
version: "0.1.0",
|
|
233
|
+
sourceRef: "t",
|
|
234
|
+
contentHash: hash,
|
|
235
|
+
prefixes: {},
|
|
236
|
+
createdAt: "2026-09-10T21:49:00.411Z",
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
mkdirSync(dirname(activePackPath(cwd)), { recursive: true });
|
|
240
|
+
writeFileSync(activePackPath(cwd), hash);
|
|
241
|
+
|
|
242
|
+
const pkgRefs = await checkPackageRefs(bootRuntime(FLAGS, cwd));
|
|
243
|
+
expect(pkgRefs.status).toBe("pass");
|
|
244
|
+
expect(pkgRefs.detail).toContain("shipped with the CLI");
|
|
245
|
+
expect(pkgRefs.detail).toContain(
|
|
246
|
+
"a pack built by pragma 0.1.0 on 2026-09-10 is ignored",
|
|
247
|
+
);
|
|
248
|
+
expect(pkgRefs.detail).toContain("pragma sources reset");
|
|
249
|
+
expect(pkgRefs.remedy).toBeUndefined();
|
|
250
|
+
});
|
|
251
|
+
|
|
215
252
|
it("a project with its OWN packs and nothing built is an attributable fail", async () => {
|
|
216
253
|
// The install that must never read healthy: `origins.packs` is "project",
|
|
217
254
|
// so the embedded snapshot is a DIFFERENT graph and every read throws
|
|
@@ -27,6 +27,8 @@ const doctorVerb: VerbSpec<Record<string, unknown>, DoctorData> = {
|
|
|
27
27
|
// (`toolDescriptions.test.ts`), so the inventory's widening is described as
|
|
28
28
|
// "the verbose global flag" rather than by its spelling.
|
|
29
29
|
doc: "Reports the environment checks first, then one row per setup target — once for your home directory, once for this project. Each row is pass, fail, available (an optional integration you have not set up yet), or skip (nothing to do here, and the row says why), with the next step printed inline. Every row is named after the setup target that repairs it, except `harnesses`: a listing, per scope, of the AI harnesses found on this machine and whether this CLI's MCP server is registered in each — the ones actually found, or every harness it knows about under the verbose global flag. Needs no store; the store check boots lazily and never fails the run.",
|
|
30
|
+
useWhen: "when something is not working, to find out what",
|
|
31
|
+
category: "diagnostic",
|
|
30
32
|
params: [],
|
|
31
33
|
output: { formatters: doctorFormatters },
|
|
32
34
|
examples: [
|
|
@@ -174,6 +174,11 @@ describe("pack list empty-state (U5, PROTECTED)", () => {
|
|
|
174
174
|
const notice = formatters.notice?.(empty);
|
|
175
175
|
expect(notice).toContain(`No ${pack.noun} entries found.`);
|
|
176
176
|
expect(notice).toMatch(hint);
|
|
177
|
+
// Over MCP the same hint ends in a TOOL CALL: an agent cannot run a
|
|
178
|
+
// command, and `meta.notice` is the only place it reads this.
|
|
179
|
+
const overMcp = formatters.notice?.(empty, "mcp");
|
|
180
|
+
expect(overMcp).toMatch(/Call `[a-z]+_[a-z]+ \{.*\}`\./);
|
|
181
|
+
expect(overMcp).not.toMatch(hint);
|
|
177
182
|
// JSON is the uniform empty array — unchanged by the message.
|
|
178
183
|
expect(formatters.json(empty)).toBe("[]");
|
|
179
184
|
// The llm view stays non-blank too (the `(0)` heading plus the message).
|
|
@@ -103,6 +103,8 @@ const connectVerb: VerbSpec<Record<string, unknown>, ConnectResult> = {
|
|
|
103
103
|
path: ["graph", "connect"],
|
|
104
104
|
summary: "Show the shortest relation paths between two entities.",
|
|
105
105
|
doc: "Finds every shortest path of RELATION edges between two entities, and says which kind of nothing it found when there is no path. An edge counts as a relation only where it does not fan out into a roster — membership of a class, a tier or a family is answered by that noun's list verb, not by a path — so most pairs are correctly reported as unconnected. Address each endpoint by prefixed name (ds:global.component.button) or absolute IRI. Every answer states the hop limit, the fan-in threshold, and the packs searched.",
|
|
106
|
+
useWhen: "when asked whether and how two things are related",
|
|
107
|
+
example: { a: "ds:global.component.button", b: "dt:color.text" },
|
|
106
108
|
params: [
|
|
107
109
|
{
|
|
108
110
|
kind: "string",
|
|
@@ -11,6 +11,7 @@ import { toTurtle } from "../../kernel/render/turtle.js";
|
|
|
11
11
|
import type {
|
|
12
12
|
InboundGroup,
|
|
13
13
|
InspectResult,
|
|
14
|
+
PredicateGroup,
|
|
14
15
|
ReadTerm,
|
|
15
16
|
} from "../../kernel/runtime/readEntity.js";
|
|
16
17
|
import type { Formatters } from "../../kernel/spec/index.js";
|
|
@@ -39,6 +40,13 @@ function inboundHeading(group: InboundGroup): string {
|
|
|
39
40
|
return `${short(group.predicate)} (${suffix})`;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/** `predicate (24 objects, showing 10)` — a cut group says what it was cut from. */
|
|
44
|
+
function outboundHeading(group: PredicateGroup): string {
|
|
45
|
+
return group.truncated
|
|
46
|
+
? `${short(group.predicate)} (${group.count} objects, showing ${group.objects.length})`
|
|
47
|
+
: short(group.predicate);
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
export const inspectFormatters: Formatters<InspectResult> = {
|
|
43
51
|
plain(data) {
|
|
44
52
|
const title = data.label
|
|
@@ -46,7 +54,7 @@ export const inspectFormatters: Formatters<InspectResult> = {
|
|
|
46
54
|
: (data.prefixed ?? data.uri);
|
|
47
55
|
const lines = [title, "═".repeat(Math.max(title.length, 24)), ""];
|
|
48
56
|
for (const group of data.groups) {
|
|
49
|
-
lines.push(` ${
|
|
57
|
+
lines.push(` ${outboundHeading(group)}:`);
|
|
50
58
|
for (const object of group.objects) lines.push(` ${named(object)}`);
|
|
51
59
|
}
|
|
52
60
|
for (const [via, rows] of Object.entries(data.nested)) {
|
|
@@ -333,3 +333,76 @@ describe("bounds and losslessness the review surfaced", () => {
|
|
|
333
333
|
});
|
|
334
334
|
});
|
|
335
335
|
});
|
|
336
|
+
|
|
337
|
+
describe("an outbound predicate is cut below `detailed`, and says so", () => {
|
|
338
|
+
// One subject asserting a wide predicate both ways: 14 named objects, and 12
|
|
339
|
+
// blank ones that inline as records.
|
|
340
|
+
const indexes = Array.from({ length: 14 }, (_, index) => index);
|
|
341
|
+
const WIDE_TTL = [
|
|
342
|
+
"@prefix ds: <https://ds.canonical.com/> .",
|
|
343
|
+
`ds:wide ds:name "Wide" ;`,
|
|
344
|
+
` ds:uses ${indexes.map((index) => `ds:part.${index}`).join(", ")} ;`,
|
|
345
|
+
` ds:binds ${indexes
|
|
346
|
+
.slice(0, 12)
|
|
347
|
+
.map((index) => `[ ds:rank ${index} ]`)
|
|
348
|
+
.join(", ")} .`,
|
|
349
|
+
].join("\n");
|
|
350
|
+
|
|
351
|
+
const readAt = async (
|
|
352
|
+
detail: "summary" | "standard" | "detailed",
|
|
353
|
+
): Promise<InspectResult> => {
|
|
354
|
+
const fixture = await buildFixtureRuntime({
|
|
355
|
+
ttl: WIDE_TTL,
|
|
356
|
+
prefixes: BLOCK_PREFIXES,
|
|
357
|
+
detail,
|
|
358
|
+
});
|
|
359
|
+
try {
|
|
360
|
+
return (await inspectVerb.run(
|
|
361
|
+
{ uri: "ds:wide" },
|
|
362
|
+
fixture.rt,
|
|
363
|
+
)) as InspectResult;
|
|
364
|
+
} finally {
|
|
365
|
+
(await fixture.rt.store.get()).store.dispose();
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const group = (result: InspectResult, local: string) =>
|
|
369
|
+
result.groups.find((g) => g.predicate.value === `${DS}${local}`);
|
|
370
|
+
|
|
371
|
+
it("lists the head and states the true count at standard", async () => {
|
|
372
|
+
const result = await readAt("standard");
|
|
373
|
+
expect(group(result, "uses")).toMatchObject({ truncated: true, count: 14 });
|
|
374
|
+
expect(group(result, "uses")?.objects).toHaveLength(10);
|
|
375
|
+
// The inlined records follow the objects that survived the cut.
|
|
376
|
+
expect(group(result, "binds")?.count).toBe(12);
|
|
377
|
+
expect(result.nested["ds:binds"]).toHaveLength(10);
|
|
378
|
+
// A predicate under the cap carries neither marker.
|
|
379
|
+
expect(group(result, "name")).not.toHaveProperty("truncated");
|
|
380
|
+
expect(group(result, "name")).not.toHaveProperty("count");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("keeps a few at summary rather than none", async () => {
|
|
384
|
+
expect(group(await readAt("summary"), "uses")?.objects).toHaveLength(3);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("cuts nothing at detailed", async () => {
|
|
388
|
+
const result = await readAt("detailed");
|
|
389
|
+
expect(group(result, "uses")?.objects).toHaveLength(14);
|
|
390
|
+
expect(group(result, "uses")).not.toHaveProperty("truncated");
|
|
391
|
+
expect(result.nested["ds:binds"]).toHaveLength(12);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("prints the count in the plain and the Turtle forms", async () => {
|
|
395
|
+
const result = await readAt("standard");
|
|
396
|
+
const formatters = inspectVerb.output.formatters;
|
|
397
|
+
expect(formatters.plain(result)).toContain(
|
|
398
|
+
"ds:uses (14 objects, showing 10):",
|
|
399
|
+
);
|
|
400
|
+
const turtle = formatters.llm(result);
|
|
401
|
+
expect(turtle).toContain(
|
|
402
|
+
" # ds:uses — 14 objects, showing 10\n ds:uses ",
|
|
403
|
+
);
|
|
404
|
+
expect(turtle).toContain(
|
|
405
|
+
" # ds:binds — 12 objects, showing 10\n ds:binds\n",
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
@@ -25,6 +25,8 @@ const inspectVerb: VerbSpec<Record<string, unknown>, InspectResult> = {
|
|
|
25
25
|
summary:
|
|
26
26
|
"Show every triple where a URI is the subject, grouped by predicate.",
|
|
27
27
|
doc: "Inspect one entity: all predicate/object pairs asserted on the subject. Address it by prefixed name (ds:global.component.button) or absolute IRI.",
|
|
28
|
+
useWhen: "when asked for everything recorded about one identifier",
|
|
29
|
+
example: { uri: "ds:global.component.button" },
|
|
28
30
|
params: [
|
|
29
31
|
{
|
|
30
32
|
kind: "string",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { queryFormatters } from "./query.render.js";
|
|
3
|
+
|
|
4
|
+
const EMPTY = { type: "select", bindings: [] } as never;
|
|
5
|
+
|
|
6
|
+
describe("graph query — zero rows names the next call", () => {
|
|
7
|
+
it("says the query ran, and points at the catalogue and the namespaces as commands on the CLI", () => {
|
|
8
|
+
const notice = queryFormatters.notice?.(EMPTY) as string;
|
|
9
|
+
expect(notice).toContain("the query ran and matched nothing");
|
|
10
|
+
expect(notice).toContain("`pragma capabilities`");
|
|
11
|
+
expect(notice).toContain("`pragma ontology list`");
|
|
12
|
+
// The condensed form is one stream, so it carries the same sentence.
|
|
13
|
+
expect(queryFormatters.llm(EMPTY)).toBe(notice);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("spells them as tool calls over MCP, and names no domain tool", () => {
|
|
17
|
+
const notice = queryFormatters.notice?.(EMPTY, "mcp") as string;
|
|
18
|
+
expect(notice).toContain("`capabilities {}`");
|
|
19
|
+
expect(notice).toContain("`ontology_list {}`");
|
|
20
|
+
expect(notice).not.toContain("pragma ");
|
|
21
|
+
// Which tool answers the question is the catalogue's to say, not this
|
|
22
|
+
// renderer's: the only tools it may name are the two it points at.
|
|
23
|
+
const named = [...notice.matchAll(/`([a-z_]+) \{/g)].map((m) => m[1]);
|
|
24
|
+
expect(named.sort()).toEqual(["capabilities", "ontology_list"]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("says nothing when there are rows", () => {
|
|
28
|
+
expect(
|
|
29
|
+
queryFormatters.notice?.({
|
|
30
|
+
type: "select",
|
|
31
|
+
bindings: [{ s: "ds:x" }],
|
|
32
|
+
} as never),
|
|
33
|
+
).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
* - json: the serialized {@link QueryResult}.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import
|
|
12
|
+
import { quoteCall } from "../../kernel/spec/call.js";
|
|
13
|
+
import type { Call, Formatters, Surface } from "../../kernel/spec/index.js";
|
|
13
14
|
|
|
14
15
|
// Inline `import("…")` type (no `from`) — keeps the ke types off the static
|
|
15
16
|
// import graph the lazy-dispatch probe walks (see query.verb.ts).
|
|
16
|
-
type
|
|
17
|
+
type QueryAnswer = import("./runQuery.js").QueryAnswer;
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Render CONSTRUCT triples as readable `subject\tpredicate\tobject` lines,
|
|
@@ -35,13 +36,46 @@ function formatTriples(
|
|
|
35
36
|
.join("\n");
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Where an empty SELECT points. Deliberately no domain tool: which one answers
|
|
41
|
+
* the caller's question is the catalogue's to say, so the pointer is to the
|
|
42
|
+
* catalogue — and to the namespaces that exist, since an invented prefix parses
|
|
43
|
+
* and matches nothing.
|
|
44
|
+
*/
|
|
45
|
+
export const EMPTY_QUERY_CALLS = {
|
|
46
|
+
catalogue: { verb: "capabilities" },
|
|
47
|
+
namespaces: { verb: "ontology list" },
|
|
48
|
+
} as const satisfies Record<string, Call>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* What zero rows says for itself. "No results." read as a failed tool to an
|
|
52
|
+
* agent with nowhere to go next. The query RAN; when its own text declares a
|
|
53
|
+
* prefix this graph does not have, that is the reason and it is said exactly.
|
|
54
|
+
*/
|
|
55
|
+
function describeEmptySelect(result: QueryAnswer, surface: Surface): string {
|
|
56
|
+
const { catalogue, namespaces } = EMPTY_QUERY_CALLS;
|
|
57
|
+
const invented = (result.unknownPrefixes ?? [])
|
|
58
|
+
.map(
|
|
59
|
+
({ prefix, iri }) =>
|
|
60
|
+
`Prefix \`${prefix}:\` <${iri}> is not a namespace of this graph, so nothing under it can match.`,
|
|
61
|
+
)
|
|
62
|
+
.join(" ");
|
|
63
|
+
return [
|
|
64
|
+
"No results: the query ran and matched nothing.",
|
|
65
|
+
invented,
|
|
66
|
+
`The namespaces that exist are listed by ${quoteCall(namespaces, surface)} (an invented prefix matches nothing). A domain tool may answer this without SPARQL: ${quoteCall(catalogue, surface)} lists the tools by the question each answers.`,
|
|
67
|
+
]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(" ");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const queryFormatters: Formatters<QueryAnswer> = {
|
|
39
73
|
// Zero rows/triples: plain stdout stays empty — the notice is
|
|
40
74
|
// `notice`, routed to stderr (exit 0) by the dispatcher so a pipe
|
|
41
75
|
// reads no prose. ASK always has a result and never goes empty.
|
|
42
|
-
notice(result) {
|
|
76
|
+
notice(result, surface = "cli") {
|
|
43
77
|
if (result.type === "select" && result.bindings.length === 0) {
|
|
44
|
-
return
|
|
78
|
+
return describeEmptySelect(result, surface);
|
|
45
79
|
}
|
|
46
80
|
if (result.type === "construct" && result.triples.length === 0) {
|
|
47
81
|
return "No triples.";
|
|
@@ -65,7 +99,9 @@ export const queryFormatters: Formatters<QueryResult> = {
|
|
|
65
99
|
|
|
66
100
|
llm(result) {
|
|
67
101
|
if (result.type === "select") {
|
|
68
|
-
if (result.bindings.length === 0)
|
|
102
|
+
if (result.bindings.length === 0) {
|
|
103
|
+
return describeEmptySelect(result, "cli");
|
|
104
|
+
}
|
|
69
105
|
const cols = Object.keys(result.bindings[0] ?? {});
|
|
70
106
|
const header = `| ${cols.join(" | ")} |`;
|
|
71
107
|
const divider = `| ${cols.map(() => "---").join(" | ")} |`;
|
|
@@ -84,6 +120,8 @@ export const queryFormatters: Formatters<QueryResult> = {
|
|
|
84
120
|
},
|
|
85
121
|
|
|
86
122
|
json(result) {
|
|
87
|
-
|
|
123
|
+
// The payload is the engine's own shape; the prefix finding is the notice's.
|
|
124
|
+
const { unknownPrefixes: _unknownPrefixes, ...data } = result;
|
|
125
|
+
return JSON.stringify(data);
|
|
88
126
|
},
|
|
89
127
|
};
|
|
@@ -64,3 +64,47 @@ describe("a name pragma printed can be pasted back (PROTECTED)", () => {
|
|
|
64
64
|
expect(prefixed).toEqual(absolute);
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
describe("an invented namespace is named, not answered with silence", () => {
|
|
69
|
+
// The recorded failure: nine queries against a namespace that does not
|
|
70
|
+
// exist, nine "No results", and a guess. A declared PREFIX always parses, so
|
|
71
|
+
// only a check against the store's own prefix map can say why nothing matched.
|
|
72
|
+
const INVENTED =
|
|
73
|
+
"PREFIX tok: <https://example.invalid/tokens#> SELECT ?c WHERE { ?c tok:uses ?t }";
|
|
74
|
+
|
|
75
|
+
it("says exactly which prefix is not a namespace of this graph, on both surfaces", async () => {
|
|
76
|
+
const result = await queryVerb.run({ sparql: INVENTED }, rt);
|
|
77
|
+
const { notice, json } = queryVerb.output.formatters;
|
|
78
|
+
const cli = notice?.(result, "cli") as string;
|
|
79
|
+
expect(cli).toContain(
|
|
80
|
+
"Prefix `tok:` <https://example.invalid/tokens#> is not a namespace of this graph",
|
|
81
|
+
);
|
|
82
|
+
expect(cli).toContain("`pragma ontology list`");
|
|
83
|
+
expect(notice?.(result, "mcp")).toContain("`ontology_list {}`");
|
|
84
|
+
// The finding explains the emptiness; the payload stays the engine's shape.
|
|
85
|
+
const payload = JSON.parse(json(result));
|
|
86
|
+
expect(payload).toMatchObject({ type: "select", bindings: [] });
|
|
87
|
+
expect(payload).not.toHaveProperty("unknownPrefixes");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("says nothing about prefixes when the query declares only real ones, or has rows", async () => {
|
|
91
|
+
const real = await queryVerb.run(
|
|
92
|
+
{
|
|
93
|
+
sparql:
|
|
94
|
+
"PREFIX ds: <https://ds.canonical.com/> SELECT ?s WHERE { ?s a ds:NoSuchClass }",
|
|
95
|
+
},
|
|
96
|
+
rt,
|
|
97
|
+
);
|
|
98
|
+
const notice = queryVerb.output.formatters.notice?.(real, "cli") as string;
|
|
99
|
+
expect(notice).toContain("the query ran and matched nothing");
|
|
100
|
+
expect(notice).not.toContain("is not a namespace");
|
|
101
|
+
const hit = await queryVerb.run(
|
|
102
|
+
{
|
|
103
|
+
sparql: `${INVENTED.replace("WHERE { ?c tok:uses ?t }", "WHERE { ?c ?p ?t } LIMIT 1")}`,
|
|
104
|
+
},
|
|
105
|
+
rt,
|
|
106
|
+
);
|
|
107
|
+
expect(queryVerb.output.formatters.notice?.(hit, "cli")).toBeUndefined();
|
|
108
|
+
expect(hit).not.toHaveProperty("unknownPrefixes");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -24,6 +24,8 @@ const queryVerb: VerbSpec<Record<string, unknown>, QueryResult> = {
|
|
|
24
24
|
path: ["graph", "query"],
|
|
25
25
|
summary: "Run a raw SPARQL query against the loaded graph.",
|
|
26
26
|
doc: "Executes an arbitrary SPARQL query (SELECT / ASK / CONSTRUCT) against the store. Prefixes are applied automatically from the pack's namespace map; list the ontology namespaces to discover the available prefixes.",
|
|
27
|
+
useWhen: "only when no other tool answers: SPARQL joins or counts",
|
|
28
|
+
example: { sparql: "SELECT ?s WHERE { ?s a ds:Component } LIMIT 5" },
|
|
27
29
|
params: [
|
|
28
30
|
{
|
|
29
31
|
kind: "string",
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, expect, it } from "vitest";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
expandPrefixedNames,
|
|
15
|
+
findUnknownPrefixes,
|
|
16
|
+
trimQueryError,
|
|
17
|
+
} from "./queryText.js";
|
|
14
18
|
|
|
15
19
|
const PREFIXES = {
|
|
16
20
|
ds: "https://ds.canonical.com/",
|
|
@@ -119,3 +123,53 @@ describe("trimQueryError — the caller's own position", () => {
|
|
|
119
123
|
);
|
|
120
124
|
});
|
|
121
125
|
});
|
|
126
|
+
|
|
127
|
+
describe("findUnknownPrefixes — only a prefix that can explain the emptiness", () => {
|
|
128
|
+
const KNOWN = { ds: "https://ds.canonical.com/" };
|
|
129
|
+
const BAD = "https://example.invalid/t#";
|
|
130
|
+
|
|
131
|
+
it("reports a declared, used prefix whose IRI is no namespace of the graph", () => {
|
|
132
|
+
expect(
|
|
133
|
+
findUnknownPrefixes(
|
|
134
|
+
`PREFIX tok: <${BAD}> PREFIX ds: <https://ds.canonical.com/> SELECT ?c WHERE { ?c tok:uses ds:x }`,
|
|
135
|
+
KNOWN,
|
|
136
|
+
),
|
|
137
|
+
).toEqual([{ prefix: "tok", iri: BAD }]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("ignores a PREFIX in a comment or inside a string literal", () => {
|
|
141
|
+
expect(
|
|
142
|
+
findUnknownPrefixes(
|
|
143
|
+
`# PREFIX tok: <${BAD}>\nSELECT ?s WHERE { ?s ds:label "PREFIX tok: <${BAD}> tok:x" }`,
|
|
144
|
+
KNOWN,
|
|
145
|
+
),
|
|
146
|
+
).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("ignores a declared prefix the body never uses", () => {
|
|
150
|
+
expect(
|
|
151
|
+
findUnknownPrefixes(
|
|
152
|
+
`PREFIX tok: <${BAD}> SELECT ?s WHERE { ?s a ds:Component }`,
|
|
153
|
+
KNOWN,
|
|
154
|
+
),
|
|
155
|
+
).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("does not judge a relative IRI, which BASE resolves", () => {
|
|
159
|
+
expect(
|
|
160
|
+
findUnknownPrefixes(
|
|
161
|
+
"BASE <https://ds.canonical.com/> PREFIX rel: <tokens/> SELECT ?s WHERE { ?s rel:p ?o }",
|
|
162
|
+
KNOWN,
|
|
163
|
+
),
|
|
164
|
+
).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("reports the default prefix too, when the body uses it", () => {
|
|
168
|
+
expect(
|
|
169
|
+
findUnknownPrefixes(
|
|
170
|
+
`PREFIX : <${BAD}> SELECT ?s WHERE { ?s :p ?o }`,
|
|
171
|
+
KNOWN,
|
|
172
|
+
),
|
|
173
|
+
).toEqual([{ prefix: "", iri: BAD }]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -238,3 +238,50 @@ function cut(text: string): string {
|
|
|
238
238
|
? text
|
|
239
239
|
: `${text.slice(0, MAX_PARSER_DETAIL - 1).trimEnd()}…`;
|
|
240
240
|
}
|
|
241
|
+
|
|
242
|
+
/** A `PREFIX` the caller declared whose IRI is none of the store's namespaces. */
|
|
243
|
+
export interface UnknownPrefix {
|
|
244
|
+
readonly prefix: string;
|
|
245
|
+
readonly iri: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Find the prefixes a query declares, USES, and binds to an IRI the store does
|
|
250
|
+
* not know as a namespace.
|
|
251
|
+
*
|
|
252
|
+
* A declared prefix always parses, so an invented one fails silently: every
|
|
253
|
+
* pattern under it simply matches nothing, and the caller reads "no results"
|
|
254
|
+
* as "no such data". Checked against the store's own prefix map, this is the
|
|
255
|
+
* one emptiness the query's text can explain.
|
|
256
|
+
*
|
|
257
|
+
* Read through the same {@link SCANNER} the expander uses, so a `PREFIX` in a
|
|
258
|
+
* comment or inside a string literal is not a declaration. A declared prefix
|
|
259
|
+
* the body never uses cannot be why nothing matched, and a relative IRI
|
|
260
|
+
* (resolved against `BASE`) cannot be judged from the text, so neither is
|
|
261
|
+
* reported.
|
|
262
|
+
*
|
|
263
|
+
* @param sparql - The query text as the caller wrote it.
|
|
264
|
+
* @param prefixes - The store's prefix map (prefix → namespace IRI).
|
|
265
|
+
* @returns The used declarations whose IRI is not a known namespace, in order.
|
|
266
|
+
*/
|
|
267
|
+
export function findUnknownPrefixes(
|
|
268
|
+
sparql: string,
|
|
269
|
+
prefixes: Readonly<Record<string, string>>,
|
|
270
|
+
): UnknownPrefix[] {
|
|
271
|
+
const known = new Set(Object.values(prefixes));
|
|
272
|
+
const code = sparql.replace(SCANNER, (token) =>
|
|
273
|
+
/^[#"']/.test(token) ? " " : token,
|
|
274
|
+
);
|
|
275
|
+
const body = code.replace(PREFIX_DECLARATION, " ");
|
|
276
|
+
const used = new Set(
|
|
277
|
+
[...body.matchAll(/(?:^|[^\w<:.-])([A-Za-z][\w.-]*)?:(?!\/\/)/g)].map(
|
|
278
|
+
(match) => match[1] ?? "",
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
return [...code.matchAll(PREFIX_DECLARATION)]
|
|
282
|
+
.map((match) => ({ prefix: match[1] ?? "", iri: match[2] ?? "" }))
|
|
283
|
+
.filter(
|
|
284
|
+
({ prefix, iri }) =>
|
|
285
|
+
/^[a-z][a-z0-9+.-]*:/i.test(iri) && used.has(prefix) && !known.has(iri),
|
|
286
|
+
);
|
|
287
|
+
}
|