@canonical/pragma-cli 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
- package/bundled-skills/anatomy-author/SKILL.md +840 -137
- package/bundled-skills/design-auditor/SKILL.md +133 -9
- package/bundled-skills/implement-component/SKILL.md +192 -23
- package/bundled-skills/specify-component/SKILL.md +124 -12
- package/bundled-skills/specify-pattern/SKILL.md +47 -8
- package/dist/package.json +23 -20
- package/dist/pragma.conf.js +963 -40
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +14 -5
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +33 -5
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/config/show.render.js +30 -2
- package/dist/src/capabilities/config/show.render.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/graph/connect.render.js +101 -0
- package/dist/src/capabilities/graph/connect.render.js.map +1 -0
- package/dist/src/capabilities/graph/connect.types.js +21 -0
- package/dist/src/capabilities/graph/connect.types.js.map +1 -0
- package/dist/src/capabilities/graph/connect.verb.js +147 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
- package/dist/src/capabilities/graph/index.js +5 -3
- package/dist/src/capabilities/graph/index.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +206 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -0
- package/dist/src/capabilities/graph/relationIndex.js +302 -0
- package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
- package/dist/src/capabilities/graph/runConnect.js +456 -0
- package/dist/src/capabilities/graph/runConnect.js.map +1 -0
- package/dist/src/capabilities/graph/runQuery.js +21 -4
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +6 -2
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/operations/index.js +7 -2
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/operations/writability.js +114 -0
- package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +80 -31
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +124 -42
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/renderError.js +3 -2
- package/dist/src/kernel/error/renderError.js.map +1 -1
- package/dist/src/kernel/error/validOptions.js +41 -0
- package/dist/src/kernel/error/validOptions.js.map +1 -0
- package/dist/src/kernel/packs/compile.js +90 -6
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/cursor.js +135 -0
- package/dist/src/kernel/packs/cursor.js.map +1 -0
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
- package/dist/src/kernel/packs/paging.js +108 -0
- package/dist/src/kernel/packs/paging.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +154 -5
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +118 -19
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +156 -18
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +160 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
- package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
- package/dist/src/kernel/packs/sparql/index.js +24 -14
- package/dist/src/kernel/packs/sparql/index.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
- package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
- package/dist/src/kernel/packs/storyRules.js +151 -0
- package/dist/src/kernel/packs/storyRules.js.map +1 -0
- package/dist/src/kernel/packs/tierScope.js +310 -0
- package/dist/src/kernel/packs/tierScope.js.map +1 -0
- package/dist/src/kernel/packs/types.js +80 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +10 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +104 -3
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +31 -7
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +34 -2
- package/dist/src/kernel/render/renderers.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +17 -10
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/readEntity.js +19 -4
- package/dist/src/kernel/runtime/readEntity.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +38 -5
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +23 -20
- package/pragma.conf.ts +983 -40
- package/src/capabilities/block.parity.test.ts +207 -27
- package/src/capabilities/block.shipped.exec.test.ts +156 -14
- package/src/capabilities/block.tierRank.exec.test.ts +46 -9
- package/src/capabilities/capabilities/catalog.ts +14 -5
- package/src/capabilities/capabilities/hints.ts +40 -5
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/config/set.test.ts +24 -25
- package/src/capabilities/config/show.render.test.ts +9 -4
- package/src/capabilities/config/show.render.ts +30 -2
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/distribution.parity.test.ts +12 -11
- package/src/capabilities/distribution.test.ts +77 -7
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
- package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
- package/src/capabilities/doctor/doctor.test.ts +327 -36
- package/src/capabilities/emptyRecovery.test.ts +84 -28
- package/src/capabilities/graph/connect.render.ts +141 -0
- package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
- package/src/capabilities/graph/connect.test.ts +361 -0
- package/src/capabilities/graph/connect.types.ts +136 -0
- package/src/capabilities/graph/connect.verb.ts +165 -0
- package/src/capabilities/graph/index.ts +5 -3
- package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
- package/src/capabilities/graph/query.test.ts +17 -0
- package/src/capabilities/graph/queryText.test.ts +121 -0
- package/src/capabilities/graph/queryText.ts +240 -0
- package/src/capabilities/graph/relationIndex.ts +372 -0
- package/src/capabilities/graph/runConnect.ts +577 -0
- package/src/capabilities/graph/runQuery.ts +21 -6
- package/src/capabilities/lazy.test.ts +32 -2
- package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
- package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
- package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/buildPlan.ts +7 -1
- package/src/capabilities/setup/operations/index.ts +14 -0
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
- package/src/capabilities/setup/operations/setupLsp.ts +544 -85
- package/src/capabilities/setup/operations/setupMcp.ts +212 -4
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/operations/writability.test.ts +151 -0
- package/src/capabilities/setup/operations/writability.ts +135 -0
- package/src/capabilities/setup/plan.ts +11 -0
- package/src/capabilities/setup/setup.test.ts +992 -16
- package/src/capabilities/setup/setup.verb.ts +84 -36
- package/src/capabilities/setup/targets.ts +152 -48
- package/src/capabilities/setup/types.ts +41 -5
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/sources/sources.test.ts +55 -0
- package/src/capabilities/surface.test.ts +59 -11
- package/src/capabilities/tier.parity.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +784 -0
- package/src/capabilities/tokenGraph.cli.test.ts +279 -0
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
- package/src/capabilities/variable.parity.test.ts +448 -0
- package/src/constants.ts +8 -0
- package/src/kernel/completion/shellDrive.test.ts +32 -30
- package/src/kernel/config/defaults.test.ts +21 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/error/renderError.ts +3 -2
- package/src/kernel/error/validOptions.test.ts +57 -0
- package/src/kernel/error/validOptions.ts +45 -0
- package/src/kernel/packs/collect.test.ts +2 -0
- package/src/kernel/packs/compile.test.ts +188 -15
- package/src/kernel/packs/compile.ts +109 -14
- package/src/kernel/packs/cursor.ts +135 -0
- package/src/kernel/packs/filteredEmpty.test.ts +213 -0
- package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
- package/src/kernel/packs/graphqlSource.test.ts +152 -0
- package/src/kernel/packs/listNotice.test.ts +156 -0
- package/src/kernel/packs/paging.ts +110 -0
- package/src/kernel/packs/renderPack.ts +186 -13
- package/src/kernel/packs/resolveEntity.test.ts +62 -3
- package/src/kernel/packs/resolveEntity.ts +186 -26
- package/src/kernel/packs/runBodies.ts +196 -26
- package/src/kernel/packs/schema.ts +180 -13
- package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
- package/src/kernel/packs/sparql/authorQuery.ts +257 -0
- package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
- package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
- package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
- package/src/kernel/packs/sparql/filterValues.ts +182 -0
- package/src/kernel/packs/sparql/index.ts +30 -14
- package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
- package/src/kernel/packs/sparql/runSelect.ts +42 -2
- package/src/kernel/packs/sparql/searchTerm.ts +29 -0
- package/src/kernel/packs/storyRules.ts +180 -0
- package/src/kernel/packs/tierScope.test.ts +487 -0
- package/src/kernel/packs/tierScope.ts +409 -0
- package/src/kernel/packs/toolDescription.test.ts +13 -10
- package/src/kernel/packs/types.ts +311 -9
- package/src/kernel/project/cli/buildProgram.ts +17 -1
- package/src/kernel/project/cli/dispatch.test.ts +87 -0
- package/src/kernel/project/cli/dispatch.ts +114 -4
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/project/cli/suggest.test.ts +35 -0
- package/src/kernel/project/cli/suggestNames.ts +34 -7
- package/src/kernel/project/mcp/registerVerb.ts +5 -1
- package/src/kernel/render/contracts.ts +2 -0
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.test.ts +43 -0
- package/src/kernel/render/renderers.ts +34 -1
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +17 -13
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/readEntity.ts +19 -4
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +39 -5
- package/src/kernel/runtime/types.ts +30 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +17 -0
- package/src/testing/PARITY_GAPS.ts +4 -4
- package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
- package/src/testing/behavioral/completion.test.ts +7 -6
- package/src/testing/behavioral/disclosure.test.ts +39 -37
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
- package/src/testing/behavioral/journeys.cli.test.ts +71 -32
- package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
- package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
- package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
- package/src/testing/behavioral/mcpSurface.test.ts +28 -25
- package/src/testing/behavioral/parity.test.ts +25 -23
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
- package/src/testing/eval/cases/effectful.ts +7 -1
- package/src/testing/eval/cases/readNouns.ts +100 -11
- package/src/testing/fixtures/blockGraph.ts +89 -5
- package/src/testing/fixtures/connectGraph.ts +159 -0
- package/src/testing/fixtures/graph/canonical.ts +128 -32
- package/src/testing/helpers/fsProbe.ts +88 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/src/testing/perf/budgets.test.ts +3 -1
- package/src/testing/perf/budgets.ts +138 -54
- package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
- package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
- package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
- package/src/kernel/packs/sparql/applyFilters.ts +0 -203
- package/src/kernel/packs/sparql/applySearch.ts +0 -33
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A truncated list says so — in EVERY format it is read in.
|
|
3
|
+
*
|
|
4
|
+
* The page notice reached `plain` (stderr) and `json` (`meta.notice`) but never
|
|
5
|
+
* `llm`, which is the form an agent reads and the form ANY piped invocation
|
|
6
|
+
* auto-detects. So `pragma token list --type color` ended at row 300 under a
|
|
7
|
+
* heading that said `(300)`, with nothing anywhere saying a page had been cut.
|
|
8
|
+
*
|
|
9
|
+
* The heading admits it too. The total is NOT named: a page is cut with one
|
|
10
|
+
* extra row rather than a second COUNT over the filtered population, so "more
|
|
11
|
+
* exist" is the strongest claim the read can honestly make.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
15
|
+
import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
|
|
16
|
+
import { executeVerb } from "../project/cli/dispatch.js";
|
|
17
|
+
import type { GlobalFlags, PragmaRuntime } from "../runtime/types.js";
|
|
18
|
+
import type { VerbSpec } from "../spec/types.js";
|
|
19
|
+
import { compilePack } from "./compile.js";
|
|
20
|
+
import type { PackDefinition } from "./types.js";
|
|
21
|
+
import { distributionSource } from "./types.js";
|
|
22
|
+
import { verbKey } from "./uniqueness.js";
|
|
23
|
+
|
|
24
|
+
const PREFIXES = {
|
|
25
|
+
ex: "https://example.org/widgets#",
|
|
26
|
+
owl: "http://www.w3.org/2002/07/owl#",
|
|
27
|
+
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
|
|
28
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const TTL = `
|
|
32
|
+
@prefix ex: <https://example.org/widgets#> .
|
|
33
|
+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
34
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
35
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
36
|
+
|
|
37
|
+
ex:Widget a owl:Class .
|
|
38
|
+
ex:name a owl:DatatypeProperty ; rdfs:domain ex:Widget ; rdfs:range xsd:string .
|
|
39
|
+
ex:kind a owl:DatatypeProperty ; rdfs:domain ex:Widget ; rdfs:range xsd:string .
|
|
40
|
+
|
|
41
|
+
ex:button a ex:Widget ; ex:name "Button" ; ex:kind "input" .
|
|
42
|
+
ex:label a ex:Widget ; ex:name "Label" ; ex:kind "display" .
|
|
43
|
+
ex:slider a ex:Widget ; ex:name "Slider" ; ex:kind "input" .
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
/** The recovery a story authors for an EMPTY POPULATION — never for a filter. */
|
|
47
|
+
const EMPTY_RECOVERY = {
|
|
48
|
+
message: "No widgets in the store.",
|
|
49
|
+
cli: "sources update",
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
const WIDGET_LIST = {
|
|
53
|
+
query: [
|
|
54
|
+
"SELECT ?uri ?name ?kind WHERE {",
|
|
55
|
+
" ?uri a ex:Widget ; ex:name ?name .",
|
|
56
|
+
" OPTIONAL { ?uri ex:kind ?kind }",
|
|
57
|
+
"} ORDER BY ?name",
|
|
58
|
+
].join("\n"),
|
|
59
|
+
columns: [
|
|
60
|
+
{ field: "uri", label: "IRI" },
|
|
61
|
+
{ field: "name", label: "Name" },
|
|
62
|
+
{ field: "kind", label: "Kind" },
|
|
63
|
+
],
|
|
64
|
+
filters: [{ param: "kind", variable: "kind", values: ["input", "display"] }],
|
|
65
|
+
search: { variables: ["name"] },
|
|
66
|
+
emptyRecovery: EMPTY_RECOVERY,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Three rows — enough to cut a page in two. */
|
|
70
|
+
const WIDGET_PACK: PackDefinition = {
|
|
71
|
+
noun: "widget",
|
|
72
|
+
description: "List widgets.",
|
|
73
|
+
list: WIDGET_LIST,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const listVerb = (pack: PackDefinition): VerbSpec =>
|
|
77
|
+
compilePack(pack, distributionSource("test:list"), PREFIXES).find(
|
|
78
|
+
(v) => verbKey(v.path) === `${pack.noun} list`,
|
|
79
|
+
) as VerbSpec;
|
|
80
|
+
|
|
81
|
+
const REAL = { dryRun: false, undo: false, yes: false };
|
|
82
|
+
|
|
83
|
+
const PLAIN: GlobalFlags = {
|
|
84
|
+
llm: false,
|
|
85
|
+
autoLlm: false,
|
|
86
|
+
format: "plain",
|
|
87
|
+
verbose: false,
|
|
88
|
+
};
|
|
89
|
+
const LLM: GlobalFlags = { ...PLAIN, llm: true, format: "llm" };
|
|
90
|
+
const JSON_FLAGS: GlobalFlags = { ...PLAIN, format: "json" };
|
|
91
|
+
|
|
92
|
+
let rt: PragmaRuntime;
|
|
93
|
+
/** The same runtime, reading in another format. */
|
|
94
|
+
const as = (flags: GlobalFlags): PragmaRuntime => ({
|
|
95
|
+
...rt,
|
|
96
|
+
globalFlags: flags,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
beforeAll(async () => {
|
|
100
|
+
({ rt } = await buildFixtureRuntime({ ttl: TTL, prefixes: PREFIXES }));
|
|
101
|
+
});
|
|
102
|
+
afterAll(async () => {
|
|
103
|
+
(await rt.store.get()).store.dispose();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("a truncated page says so in every format", () => {
|
|
107
|
+
it("plain: the notice is the last thing the reader is told", async () => {
|
|
108
|
+
const outcome = await executeVerb(
|
|
109
|
+
listVerb(WIDGET_PACK),
|
|
110
|
+
{ limit: 2 },
|
|
111
|
+
REAL,
|
|
112
|
+
as(PLAIN),
|
|
113
|
+
);
|
|
114
|
+
expect(outcome.exitCode).toBe(0);
|
|
115
|
+
expect(outcome.stdout).toContain("Button");
|
|
116
|
+
const notice = `${outcome.stdout ?? ""}${outcome.stderr ?? ""}`;
|
|
117
|
+
expect(notice).toContain("Showing 2 widget entries, and more exist.");
|
|
118
|
+
expect(notice).toContain("--after");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("llm: the heading admits the page and the body ends with the notice", async () => {
|
|
122
|
+
const outcome = await executeVerb(
|
|
123
|
+
listVerb(WIDGET_PACK),
|
|
124
|
+
{ limit: 2 },
|
|
125
|
+
REAL,
|
|
126
|
+
as(LLM),
|
|
127
|
+
);
|
|
128
|
+
const stdout = outcome.stdout ?? "";
|
|
129
|
+
expect(stdout).toContain("## Widget (2, more exist)");
|
|
130
|
+
expect(stdout.trimEnd().split("\n").at(-1)).toContain(
|
|
131
|
+
"Showing 2 widget entries, and more exist.",
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("json: the notice keeps riding meta", async () => {
|
|
136
|
+
const outcome = await executeVerb(
|
|
137
|
+
listVerb(WIDGET_PACK),
|
|
138
|
+
{ limit: 2 },
|
|
139
|
+
REAL,
|
|
140
|
+
as(JSON_FLAGS),
|
|
141
|
+
);
|
|
142
|
+
const envelope = JSON.parse(outcome.stdout as string);
|
|
143
|
+
expect(envelope.meta.notice).toContain("and more exist");
|
|
144
|
+
expect(envelope.data).toHaveLength(2);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("a whole answer says nothing extra, in any format", async () => {
|
|
148
|
+
const plain = await executeVerb(listVerb(WIDGET_PACK), {}, REAL, as(PLAIN));
|
|
149
|
+
expect(`${plain.stdout ?? ""}${plain.stderr ?? ""}`).not.toContain(
|
|
150
|
+
"more exist",
|
|
151
|
+
);
|
|
152
|
+
const llm = await executeVerb(listVerb(WIDGET_PACK), {}, REAL, as(LLM));
|
|
153
|
+
expect(llm.stdout).toContain("## Widget (3)");
|
|
154
|
+
expect(llm.stdout).not.toContain("more exist");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a list-shaped verb answers with: how many rows, and where from.
|
|
3
|
+
*
|
|
4
|
+
* WHY A DEFAULT AT ALL. Before this, no list carried a cap: every declared
|
|
5
|
+
* story returned its whole population on every call, and an agent calling one
|
|
6
|
+
* speculatively paid for all of it. A default is what makes the cap real — a
|
|
7
|
+
* `--limit` nobody passes bounds nothing.
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS DEFAULT. It answers to TWO constraints, and it is the largest number
|
|
10
|
+
* that satisfies both.
|
|
11
|
+
*
|
|
12
|
+
* From below: it stays above the row count of every story that already existed
|
|
13
|
+
* when the page arrived (the largest, `block list`, is 252), so no answer that
|
|
14
|
+
* was whole before the pair is truncated by it now.
|
|
15
|
+
*
|
|
16
|
+
* From above: the payload budget bounds ONE answer, and the suite that enforces
|
|
17
|
+
* it requires the largest default answer to sit under 0.8 of the ceiling —
|
|
18
|
+
* 100,000 bytes — so a ceiling nothing approaches keeps catching things and one
|
|
19
|
+
* sitting on the measurement does not flake. The fattest row any declared story
|
|
20
|
+
* serialises is `token values` at 244 bytes, so 300 rows of it is about 73 KB
|
|
21
|
+
* and the gate holds with room to spare.
|
|
22
|
+
*
|
|
23
|
+
* The first constraint alone gave 500, which is what this was, and the second
|
|
24
|
+
* retired it: the token-graph stories brought populations of 745, 1,156, 1,237
|
|
25
|
+
* and 1,746 rows, and at 500 rows `token values` measured 121,776 bytes —
|
|
26
|
+
* inside the ceiling and past the gate, with no headroom left. So the number is
|
|
27
|
+
* now derived from the budget rather than from a population that no longer
|
|
28
|
+
* bounds anything.
|
|
29
|
+
*
|
|
30
|
+
* It is still a KERNEL default and not a per-story one — a story has no
|
|
31
|
+
* page-size knob, so there is exactly one number to know, and it is written
|
|
32
|
+
* here, in the help text of every list verb, and in `BUDGETS.md` beside the
|
|
33
|
+
* payload it bounds. A per-story knob is what to reach for when one story's
|
|
34
|
+
* rows are so much fatter than the rest that one number cannot serve both;
|
|
35
|
+
* that is not true of any story today.
|
|
36
|
+
*
|
|
37
|
+
* WHY A CEILING AS WELL. The page's two integers are emitted into the query
|
|
38
|
+
* ({@link ./sparql/buildListQuery}), and the store's own `LIMIT`/`OFFSET` must
|
|
39
|
+
* fit a 32-bit integer while `Number.isInteger` admits anything up to 2^53. So
|
|
40
|
+
* `--limit 9007199254740991` — the natural "give me everything" an agent
|
|
41
|
+
* writes — used to reach the engine and come back as a raw parse error wrapped
|
|
42
|
+
* in INTERNAL_ERROR and "report this issue", from a flag whose own help invited
|
|
43
|
+
* a larger number. {@link MAX_LIST_WINDOW} is where both halves of the window
|
|
44
|
+
* are refused instead, with the typed INVALID_INPUT the rest of the pair uses
|
|
45
|
+
* and the ceiling named in the message.
|
|
46
|
+
*
|
|
47
|
+
* The cursor that carries a caller from one page to the next is
|
|
48
|
+
* {@link ./cursor}, kept apart because it hashes and this module is reached
|
|
49
|
+
* from the compiler on the storeless `--help` path. It bounds its own offset by
|
|
50
|
+
* the same ceiling, from here.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { PragmaError } from "../error/index.js";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Rows a list-shaped verb returns when the caller names no `--limit`.
|
|
57
|
+
*
|
|
58
|
+
* Derived from the payload budget and from the populations that predate the
|
|
59
|
+
* page — see the module note for both halves of the arithmetic.
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_LIST_LIMIT = 300;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The largest `--limit`, and the largest cursor offset, the kernel admits.
|
|
65
|
+
*
|
|
66
|
+
* NOT the engine's limit, which is an implementation detail of the pinned store
|
|
67
|
+
* and would make the refusal a fact about oxigraph. It comes from the payload
|
|
68
|
+
* budget instead, which is a fact about the answer: `BUDGETS.md`'s
|
|
69
|
+
* `LIST_PAYLOAD_BUDGET_BYTES` is 125,000, and the narrowest row a story can
|
|
70
|
+
* possibly serialise is `{}` plus its separating comma — three bytes — so no
|
|
71
|
+
* answer of more than 41,666 rows can be inside the budget whatever a story's
|
|
72
|
+
* columns are. A limit that cannot produce a legal answer is not a legal limit;
|
|
73
|
+
* rounded down to a number a message can carry, that is 40,000. It stays 22×
|
|
74
|
+
* the largest population the distribution ships (1,746) and 133× the default
|
|
75
|
+
* page, so it refuses nothing a caller could want today.
|
|
76
|
+
*
|
|
77
|
+
* The cursor's OFFSET takes the same ceiling, for a reason of its own: an offset
|
|
78
|
+
* is a count of rows already answered, and a walk that has passed 40,000 of
|
|
79
|
+
* them is walking a population more than two orders of magnitude larger than
|
|
80
|
+
* any single legal answer — the point at which the cursor to spend is a keyset
|
|
81
|
+
* cursor, which is what the encoding's `v` field exists to allow. Until such a
|
|
82
|
+
* story exists, an offset that large is a hand-built or tampered token, and one
|
|
83
|
+
* typed refusal is the honest answer to both.
|
|
84
|
+
*
|
|
85
|
+
* `listBudget.shipped.exec.test.ts` asserts the arithmetic tying this to the
|
|
86
|
+
* budget, so the two cannot drift apart in silence.
|
|
87
|
+
*/
|
|
88
|
+
export const MAX_LIST_WINDOW = 40_000;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read the `--limit`/`limit` argument.
|
|
92
|
+
*
|
|
93
|
+
* @param provided - The raw argument, if any.
|
|
94
|
+
* @returns The requested limit, or {@link DEFAULT_LIST_LIMIT}.
|
|
95
|
+
* @throws PragmaError INVALID_INPUT for anything that is not a whole number
|
|
96
|
+
* between 1 and {@link MAX_LIST_WINDOW}. The ceiling is named in the message:
|
|
97
|
+
* a refusal that does not say what it would accept is a guessing game.
|
|
98
|
+
*/
|
|
99
|
+
export function readLimit(provided: unknown): number {
|
|
100
|
+
if (provided === undefined) return DEFAULT_LIST_LIMIT;
|
|
101
|
+
const value = typeof provided === "number" ? provided : Number(provided);
|
|
102
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_LIST_WINDOW) {
|
|
103
|
+
throw PragmaError.invalidInput("limit", String(provided), {
|
|
104
|
+
recovery: {
|
|
105
|
+
message: `--limit takes a whole number of rows, 1 to ${MAX_LIST_WINDOW} (default ${DEFAULT_LIST_LIMIT}).`,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
@@ -26,13 +26,17 @@ import {
|
|
|
26
26
|
renderLookupPlain,
|
|
27
27
|
} from "../render/renderers.js";
|
|
28
28
|
import type { Formatters } from "../spec/index.js";
|
|
29
|
+
import { kebabCase } from "../spec/index.js";
|
|
29
30
|
import type { LookupOutput } from "./resolveEntity.js";
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
import {
|
|
32
|
+
EVERY_TIER,
|
|
33
|
+
type PackAppliedFilter,
|
|
34
|
+
type PackChildRow,
|
|
35
|
+
type PackEntity,
|
|
36
|
+
type PackList,
|
|
37
|
+
type PackLookup,
|
|
38
|
+
type PackPage,
|
|
39
|
+
type PackRow,
|
|
36
40
|
} from "./types.js";
|
|
37
41
|
|
|
38
42
|
/** Sample output: the drawn exemplars, the population size, and agent follow-ups. */
|
|
@@ -57,11 +61,31 @@ export interface RenderMeta {
|
|
|
57
61
|
*/
|
|
58
62
|
const DEFAULT_EMPTY_HINT = `Either nothing matched — try a wider filter — or the store has nothing in it yet: build it with \`${BIN_NAME} sources update\`.`;
|
|
59
63
|
|
|
60
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Build the list formatters for a list-shaped verb (list or an extra verb).
|
|
66
|
+
*
|
|
67
|
+
* Every mode renders the page's ROWS, so the three output contracts are exactly
|
|
68
|
+
* what they were before a list carried a page: `plain` a table, `llm` its
|
|
69
|
+
* frozen condensed form, `json` the bare array. What the page adds rides the
|
|
70
|
+
* `notice` seam — the channel that already exists for what the data cannot say
|
|
71
|
+
* about itself, and which both machine surfaces project into `meta.notice`.
|
|
72
|
+
*
|
|
73
|
+
* EVERY format says it, though, because a truncation nobody is told about is a
|
|
74
|
+
* wrong answer in any dialect. The seam alone did not reach `llm`: the
|
|
75
|
+
* dispatcher routes a notice to stderr in `plain` and into `meta.notice` in
|
|
76
|
+
* `json`, but the `llm` branch renders the body and nothing else — and `llm`
|
|
77
|
+
* is what an agent (or any piped invocation, which auto-detects it) actually
|
|
78
|
+
* reads. So the condensed form carries the sentence in its own body, as a final
|
|
79
|
+
* line, and its heading admits the page is a page.
|
|
80
|
+
*
|
|
81
|
+
* A page that is the whole answer says nothing, which is why declaring the
|
|
82
|
+
* default page size above every story's population left every existing answer
|
|
83
|
+
* untouched, notice included.
|
|
84
|
+
*/
|
|
61
85
|
export function listFormatters(
|
|
62
86
|
shape: PackList,
|
|
63
87
|
meta: RenderMeta,
|
|
64
|
-
): Formatters<
|
|
88
|
+
): Formatters<PackPage> {
|
|
65
89
|
const columns: ColumnDef<PackRow>[] = shape.columns.map((column) => ({
|
|
66
90
|
key: column.field,
|
|
67
91
|
label: column.label ?? column.field,
|
|
@@ -84,16 +108,138 @@ export function listFormatters(
|
|
|
84
108
|
emptyHint,
|
|
85
109
|
};
|
|
86
110
|
return {
|
|
87
|
-
plain: (
|
|
88
|
-
llm: (
|
|
89
|
-
|
|
111
|
+
plain: (page, context) => renderListPlain(page.rows, options, context),
|
|
112
|
+
llm: (page) => {
|
|
113
|
+
const body = renderListLlm(page.rows, emptyCopy(page, meta, options), {
|
|
114
|
+
more: page.nextAfter !== undefined,
|
|
115
|
+
...(scopeText(page) === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { scope: scopeText(page) as string }),
|
|
118
|
+
});
|
|
119
|
+
const notice = listNotice(page, meta);
|
|
120
|
+
return notice ? `${body}\n\n${notice}` : body;
|
|
121
|
+
},
|
|
122
|
+
json: (page) => JSON.stringify(page.rows, null, 2),
|
|
90
123
|
// Zero rows: the dispatcher routes this to stderr (exit 0) so the plain
|
|
91
124
|
// stdout stream stays pure data; llm/json keep their own empty shapes.
|
|
92
|
-
notice: (
|
|
93
|
-
rows.length === 0
|
|
125
|
+
notice: (page) =>
|
|
126
|
+
page.rows.length === 0
|
|
127
|
+
? joinNotices([
|
|
128
|
+
renderListEmptyNotice(emptyCopy(page, meta, options)),
|
|
129
|
+
scopeNotice(page),
|
|
130
|
+
])
|
|
131
|
+
: listNotice(page, meta),
|
|
132
|
+
// The scope rides the envelope as DATA as well as prose: an agent deciding
|
|
133
|
+
// whether to widen the read should not have to parse a sentence to learn
|
|
134
|
+
// which tiers it got.
|
|
135
|
+
meta: (page) => (page.scope ? { scope: page.scope } : undefined),
|
|
94
136
|
};
|
|
95
137
|
}
|
|
96
138
|
|
|
139
|
+
/** The tier scope in the words `--tier` accepts, or nothing when unscoped. */
|
|
140
|
+
function scopeText(page: PackPage): string | undefined {
|
|
141
|
+
return page.scope ? page.scope.tiers.join(", ") : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* What a SCOPED page says for itself: which tiers it answered from, and the
|
|
146
|
+
* argument that widens it.
|
|
147
|
+
*
|
|
148
|
+
* The plain table has no heading to carry it (a plain list is columns a pipe
|
|
149
|
+
* reads as records), so the sentence is the plain surface's whole account of
|
|
150
|
+
* the scope — and the notice seam is where this package already puts what the
|
|
151
|
+
* data cannot say about itself.
|
|
152
|
+
*/
|
|
153
|
+
function scopeNotice(page: PackPage): string | undefined {
|
|
154
|
+
const scope = scopeText(page);
|
|
155
|
+
if (scope === undefined) return undefined;
|
|
156
|
+
return (
|
|
157
|
+
`Tier scope: ${scope}. ` +
|
|
158
|
+
`Pass \`--tier <name>\` for one tier and its ancestors, or \`--tier ${EVERY_TIER}\` for every tier.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The page's notices, in the order a reader needs them: scope, then paging. */
|
|
163
|
+
function listNotice(page: PackPage, meta: RenderMeta): string | undefined {
|
|
164
|
+
return joinNotices([scopeNotice(page), pageNotice(page, meta)]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Join what a page has to say into one notice, dropping what it has not. */
|
|
168
|
+
function joinNotices(
|
|
169
|
+
notices: readonly (string | undefined)[],
|
|
170
|
+
): string | undefined {
|
|
171
|
+
const present = notices.filter((notice): notice is string => Boolean(notice));
|
|
172
|
+
return present.length === 0 ? undefined : present.join(" ");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The empty-state copy this page deserves.
|
|
177
|
+
*
|
|
178
|
+
* Two facts can empty a list and zero rows cannot tell them apart: nothing was
|
|
179
|
+
* there, or a filter missed what was. Both are worth saying and they are not
|
|
180
|
+
* exclusive, so a filtered empty page says both — the kernel's own sentence
|
|
181
|
+
* names the arguments the caller typed, and the story's `emptyRecovery` follows
|
|
182
|
+
* it on the next line, exactly as it does on an unfiltered page.
|
|
183
|
+
*
|
|
184
|
+
* It did not, briefly: a filtered empty page reported only the narrowing,
|
|
185
|
+
* because the recoveries of the day asserted an empty store ("No token symbols
|
|
186
|
+
* in the store … run `pragma sources update`") and so told a reader with 745
|
|
187
|
+
* symbols in it to rebuild for nothing. That is a WORDING defect, and it is
|
|
188
|
+
* fixed where it lives — a story's recovery is now written to hold whether the
|
|
189
|
+
* population is empty or a filter missed a populated one, or the story declares
|
|
190
|
+
* none. Suppressing it here cost more than it saved: `token consumers --symbol
|
|
191
|
+
* color.text` narrows a table that records no bindings at all, and "No token
|
|
192
|
+
* matches `--symbol color.text`." on its own read as a mistyped symbol while
|
|
193
|
+
* withholding the one account of the emptiness that verb has.
|
|
194
|
+
*
|
|
195
|
+
* @param page - The rendered page (its rows and the filters that cut them).
|
|
196
|
+
* @param meta - The noun, for the sentence.
|
|
197
|
+
* @param base - The story's own empty copy.
|
|
198
|
+
* @returns `base` unchanged, or `base` with the filter-shaped message — the
|
|
199
|
+
* story's own hint is kept either way.
|
|
200
|
+
*/
|
|
201
|
+
function emptyCopy(
|
|
202
|
+
page: PackPage,
|
|
203
|
+
meta: RenderMeta,
|
|
204
|
+
base: RenderListOptions<PackRow>,
|
|
205
|
+
): RenderListOptions<PackRow> {
|
|
206
|
+
const applied = page.filters ?? [];
|
|
207
|
+
if (page.rows.length > 0 || applied.length === 0) return base;
|
|
208
|
+
return {
|
|
209
|
+
...base,
|
|
210
|
+
emptyMessage: `No ${meta.noun} matches ${listFilters(applied)}.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The filters in force, as flags a reader can edit: `\`--kind input\`` … */
|
|
215
|
+
function listFilters(applied: readonly PackAppliedFilter[]): string {
|
|
216
|
+
const flags = applied.map(
|
|
217
|
+
(filter) => `\`--${kebabCase(filter.param)} ${filter.value}\``,
|
|
218
|
+
);
|
|
219
|
+
const last = flags.at(-1) as string;
|
|
220
|
+
return flags.length === 1
|
|
221
|
+
? last
|
|
222
|
+
: `${flags.slice(0, -1).join(", ")} and ${last}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* What a truncated page says for itself: that it is one, and the exact argument
|
|
227
|
+
* that fetches the next.
|
|
228
|
+
*
|
|
229
|
+
* The cursor is printed rather than described. An opaque token a caller has to
|
|
230
|
+
* be told how to obtain is a token they will guess at, and the two surfaces
|
|
231
|
+
* that carry this are read by agents — so the sentence contains the flag, the
|
|
232
|
+
* value, and the tool parameter, ready to copy.
|
|
233
|
+
*/
|
|
234
|
+
function pageNotice(page: PackPage, meta: RenderMeta): string | undefined {
|
|
235
|
+
if (page.nextAfter === undefined) return undefined;
|
|
236
|
+
return (
|
|
237
|
+
`Showing ${page.rows.length} ${meta.noun} entries, and more exist. ` +
|
|
238
|
+
`For the next page pass \`--after ${page.nextAfter}\` ` +
|
|
239
|
+
`(\`after\` over MCP), or raise \`--limit\` (currently ${page.limit}).`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
97
243
|
/** Build the shared per-entity render options for a lookup (reused by sample). */
|
|
98
244
|
export function lookupOptions(
|
|
99
245
|
lookup: PackLookup,
|
|
@@ -110,6 +256,7 @@ export function lookupOptions(
|
|
|
110
256
|
key: section.name,
|
|
111
257
|
heading: section.label ?? section.name,
|
|
112
258
|
kind: section.kind ?? "field",
|
|
259
|
+
...(section.note ? { note: section.note } : {}),
|
|
113
260
|
}),
|
|
114
261
|
);
|
|
115
262
|
const expandSections: SectionDef<PackEntity>[] = (lookup.expand ?? []).map(
|
|
@@ -118,6 +265,7 @@ export function lookupOptions(
|
|
|
118
265
|
heading: expand.heading ?? expand.name,
|
|
119
266
|
kind: expand.kind ?? "list",
|
|
120
267
|
...(expand.showWhenEmpty ? { showWhenEmpty: true } : {}),
|
|
268
|
+
...(expand.note ? { note: expand.note } : {}),
|
|
121
269
|
}),
|
|
122
270
|
);
|
|
123
271
|
return {
|
|
@@ -177,6 +325,29 @@ export function sampleFormatters(
|
|
|
177
325
|
};
|
|
178
326
|
}
|
|
179
327
|
|
|
328
|
+
/**
|
|
329
|
+
* What a lookup answered from OUTSIDE the tier scope says for itself.
|
|
330
|
+
*
|
|
331
|
+
* One line per name, and it is in the BODY rather than on the notice seam: the
|
|
332
|
+
* answer it explains is on stdout, and a reader piping `block lookup back-link`
|
|
333
|
+
* would otherwise see a block from a tier they are not reading with no way to
|
|
334
|
+
* tell from the output why. The machine surfaces carry the same fact
|
|
335
|
+
* structurally (`outOfScope` rides the payload), so this is one fact in two
|
|
336
|
+
* registers, not two facts.
|
|
337
|
+
*/
|
|
338
|
+
function scopeFallbackLines(
|
|
339
|
+
output: LookupOutput,
|
|
340
|
+
mode: "plain" | "llm",
|
|
341
|
+
): string[] {
|
|
342
|
+
const bullet = mode === "llm" ? "- " : "";
|
|
343
|
+
return (output.outOfScope ?? []).map(
|
|
344
|
+
(answer) =>
|
|
345
|
+
`${bullet}No "${answer.query}" in the tier scope (${answer.scope}) — ` +
|
|
346
|
+
`answering from ${answer.tiers.join(", ")}. ` +
|
|
347
|
+
`Pass \`--tier ${EVERY_TIER}\` for every tier.`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
180
351
|
/** Render each result entity, appending a compact note for any error entries. */
|
|
181
352
|
function renderOutput(
|
|
182
353
|
output: LookupOutput,
|
|
@@ -184,6 +355,8 @@ function renderOutput(
|
|
|
184
355
|
mode: "plain" | "llm" = "plain",
|
|
185
356
|
): string {
|
|
186
357
|
const bodies = output.results.map(render);
|
|
358
|
+
const fallback = scopeFallbackLines(output, mode);
|
|
359
|
+
if (fallback.length > 0) bodies.push(fallback.join("\n"));
|
|
187
360
|
if (output.errors.length > 0) {
|
|
188
361
|
const bullet = mode === "llm" ? "- " : " ";
|
|
189
362
|
const lines = output.errors.map(
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* and nothing else — as accepting a name, a prefixed name, an absolute IRI, or
|
|
8
8
|
* a glob. These assert that the resolver honours the shape it is handed rather
|
|
9
9
|
* than the source its pack declares, that an entity is addressable by IRI even
|
|
10
|
-
* when it carries no `by` value,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* when it carries no `by` value, that a name reaches its entity through the
|
|
11
|
+
* whitespace either side may be padded with, and that an ambiguous name answers
|
|
12
|
+
* with the same entity on every store and every machine — while NAMING the ones
|
|
13
|
+
* it did not answer with.
|
|
13
14
|
*
|
|
14
15
|
* That last clause is a REVERSAL, signed off by the owner. It used to read "an
|
|
15
16
|
* ambiguous name resolves to the SAME entity every time", and the suite proved
|
|
@@ -149,6 +150,64 @@ describe("pack lookup addressing (PROTECTED)", () => {
|
|
|
149
150
|
});
|
|
150
151
|
});
|
|
151
152
|
|
|
153
|
+
describe("a name resolves through the whitespace the graph carries", () => {
|
|
154
|
+
// `ds:padded.timeline` is named "Timeline " in the fixture, exactly as 66
|
|
155
|
+
// shipped names are — the transform that reads the source document keeps the
|
|
156
|
+
// cell's trailing space. That padding is not part of the name: no surface
|
|
157
|
+
// prints it and no user can type it on purpose, so a resolve it can defeat
|
|
158
|
+
// is broken for every one of those entities.
|
|
159
|
+
it("resolves a padded name by the name a user can type", async () => {
|
|
160
|
+
const out = await lookupVia(SPQ, "Timeline");
|
|
161
|
+
expect(out.errors).toEqual([]);
|
|
162
|
+
expect(uris(out)).toEqual([`${DS}padded.timeline`]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("resolves it on the graphql path too, where the resolve is the same SPARQL", async () => {
|
|
166
|
+
const out = await lookupVia(GQL, "Timeline");
|
|
167
|
+
expect(out.errors).toEqual([]);
|
|
168
|
+
expect(uris(out)).toEqual([`${DS}padded.timeline`]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("trims the ARGUMENT as well, so a pasted name reaches a clean entity", async () => {
|
|
172
|
+
// The other direction, and the reason both sides are trimmed rather than
|
|
173
|
+
// just `?name`: a name copied out of a table cell arrives padded, and
|
|
174
|
+
// Modal's own name is not.
|
|
175
|
+
const out = await lookupVia(SPQ, " Modal\t");
|
|
176
|
+
expect(out.errors).toEqual([]);
|
|
177
|
+
expect(uris(out)).toEqual([`${DS}modal`]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("still folds case while it folds the padding", async () => {
|
|
181
|
+
expect(uris(await lookupVia(SPQ, "timeline"))).toEqual([
|
|
182
|
+
`${DS}padded.timeline`,
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("keeps the entity's own name LITERAL in the answer", async () => {
|
|
187
|
+
// The trim is a matching rule, not a rendering one. What the graph holds
|
|
188
|
+
// is what a reader is shown, so the upstream correction to the transform
|
|
189
|
+
// is visible when it lands instead of being papered over here.
|
|
190
|
+
const out = await lookupVia(SPQ, "Timeline");
|
|
191
|
+
expect(out.results.at(0)?.name).toBe("Timeline ");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("suggests it by its literal name on a genuine typo", async () => {
|
|
195
|
+
// A real miss still names the padded candidate exactly as the graph holds
|
|
196
|
+
// it — the trim decides what MATCHES and changes nothing about what a
|
|
197
|
+
// reader is shown. What can no longer happen is the reported
|
|
198
|
+
// `Did you mean? - Timeline` whose only difference from the query was the
|
|
199
|
+
// padding: that case resolves now, and the suggester's own refusal to
|
|
200
|
+
// restate a query is asserted in `project/cli/suggest.test.ts`.
|
|
201
|
+
const reason = await lookupVia(SPQ, "Timelime").catch(
|
|
202
|
+
(error: unknown) => error,
|
|
203
|
+
);
|
|
204
|
+
expect(reason).toMatchObject({ code: "ENTITY_NOT_FOUND" });
|
|
205
|
+
expect((reason as { suggestions: string[] }).suggestions).toContain(
|
|
206
|
+
"Timeline ",
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
152
211
|
describe("a glob expands over the population its own shape addresses", () => {
|
|
153
212
|
it("expands an IRI-shaped glob on a sparql-sourced pack", async () => {
|
|
154
213
|
const out = await lookupVia(SPQ, "ds:*.chip");
|