@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
|
@@ -19,6 +19,80 @@
|
|
|
19
19
|
* of the canonical set.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The parameter names the KERNEL puts on a compiled verb, which a story's own
|
|
24
|
+
* filter may therefore not claim.
|
|
25
|
+
*
|
|
26
|
+
* `search` is a story's declared free-text flag; `detail` the disclosure
|
|
27
|
+
* selector; `name` and `count` a lookup's and a sample's positionals; `limit`
|
|
28
|
+
* and `after` the page every list-shaped verb carries. A filter claiming one
|
|
29
|
+
* would collide with a flag the CLI has already registered — a Commander
|
|
30
|
+
* failure outside every error envelope the CLI owns. Declared here, in the
|
|
31
|
+
* zod-free grammar, so the validator that refuses them and any reader that
|
|
32
|
+
* needs to tell a story's OWN params from the kernel's read one list.
|
|
33
|
+
*/
|
|
34
|
+
export const RESERVED_STORY_PARAMS: readonly string[] = [
|
|
35
|
+
"search",
|
|
36
|
+
"detail",
|
|
37
|
+
"name",
|
|
38
|
+
"count",
|
|
39
|
+
"limit",
|
|
40
|
+
"after",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The SPARQL VARIABLE prefix the kernel reserves, which a story's own query may
|
|
45
|
+
* therefore not use.
|
|
46
|
+
*
|
|
47
|
+
* The generated filter and search clauses bind a caller's values to variables
|
|
48
|
+
* under this prefix (`?__pragmaFilter0`, `?__pragmaSearch`). A story query using
|
|
49
|
+
* it would have its own variable shadowed, and the symptom would be a filter
|
|
50
|
+
* matching nothing with nothing raised anywhere. Beside the reserved PARAMS for
|
|
51
|
+
* the same reason: the grammar that refuses it and the builder that mints it
|
|
52
|
+
* must read one name, and this file is the zod-free place both can reach — the
|
|
53
|
+
* builder lives behind the dynamically imported run bodies, so nothing on the
|
|
54
|
+
* `--help` fast path may import it.
|
|
55
|
+
*/
|
|
56
|
+
export const RESERVED_VARIABLE_PREFIX = "__pragma";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The `--tier` value that turns the tier scope OFF — every tier, the way every
|
|
60
|
+
* read answered before the scope existed.
|
|
61
|
+
*
|
|
62
|
+
* A word rather than a second flag (this distribution retired an `--all-tiers`
|
|
63
|
+
* once already): `--tier all` answers the same question the flag already asks,
|
|
64
|
+
* so there is one argument to learn and one to document.
|
|
65
|
+
*
|
|
66
|
+
* Declared HERE, in the zod-free grammar, for the reason
|
|
67
|
+
* {@link RESERVED_VARIABLE_PREFIX} is: the compiler that puts the flag on a
|
|
68
|
+
* verb runs on the storeless `--help` fast path, and the run body that reads it
|
|
69
|
+
* lives behind a dynamic import. Both must read one spelling.
|
|
70
|
+
*/
|
|
71
|
+
export const EVERY_TIER = "all";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The parameter name the kernel puts on a TIER-SCOPED noun's reads (`--tier`,
|
|
75
|
+
* `tier` over MCP).
|
|
76
|
+
*
|
|
77
|
+
* Not in {@link RESERVED_STORY_PARAMS}, because it is reserved CONDITIONALLY:
|
|
78
|
+
* an unscoped noun may still declare a `tier` filter of its own, and one does —
|
|
79
|
+
* `variable list --tier` filters the token graph's `dt:tier`, a different
|
|
80
|
+
* predicate in a different namespace from the `ds:tier` this scope reads. The
|
|
81
|
+
* rule that a SCOPED story may not claim it lives in {@link ./storyRules}.
|
|
82
|
+
*/
|
|
83
|
+
export const TIER_PARAM = "tier";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The SELECT variable a list-shaped story publishes its entity IRI in.
|
|
87
|
+
*
|
|
88
|
+
* A convention this package already depends on twice — the condensed renderer
|
|
89
|
+
* titles a row from `uri`/`name`, and every shipped list's IRI column is `uri`
|
|
90
|
+
* — stated here because the tier scope is compiled in as a constraint on that
|
|
91
|
+
* variable, and because both the storeless declaration rules and the query
|
|
92
|
+
* builder behind the dynamic import must read one spelling of it.
|
|
93
|
+
*/
|
|
94
|
+
export const ENTITY_VARIABLE = "uri";
|
|
95
|
+
|
|
22
96
|
/** A list column: a SELECT variable to display. */
|
|
23
97
|
export interface PackColumn {
|
|
24
98
|
/** SELECT variable name (without `?`). */
|
|
@@ -53,6 +127,17 @@ export interface PackField {
|
|
|
53
127
|
export interface PackSection extends PackField {
|
|
54
128
|
/** Rendering kind (inline field or fenced code block). */
|
|
55
129
|
readonly kind?: "field" | "code";
|
|
130
|
+
/**
|
|
131
|
+
* One sentence rendered under the section's heading, above its body: how to
|
|
132
|
+
* READ what follows, when the body's own notation does not say.
|
|
133
|
+
*
|
|
134
|
+
* For a section whose body is a document the graph authored, the label is
|
|
135
|
+
* enough. For one whose body is a NOTATION — a block's anatomy is a YAML
|
|
136
|
+
* document in which `background.color: [a, b]` is a fallback chain, first
|
|
137
|
+
* value that resolves winning — the reader has to be told, and the place to
|
|
138
|
+
* tell them is where they are looking.
|
|
139
|
+
*/
|
|
140
|
+
readonly note?: string;
|
|
56
141
|
}
|
|
57
142
|
|
|
58
143
|
/** A field read from each child node of a {@link PackExpand}. */
|
|
@@ -65,6 +150,22 @@ export interface PackExpandField {
|
|
|
65
150
|
readonly label?: string;
|
|
66
151
|
/** Explicit GraphQL field name (escape hatch). Ignored on the SPARQL path. */
|
|
67
152
|
readonly graphqlField?: string;
|
|
153
|
+
/**
|
|
154
|
+
* Leave the cell BLANK when its value repeats the looked-up entity's own
|
|
155
|
+
* `by` value — for a relation an entity can point back at ITSELF along.
|
|
156
|
+
*
|
|
157
|
+
* The standing case is a block's token bindings: `ds:viaBlock` names the
|
|
158
|
+
* anatomy tree a binding was reached through, and the ontology defines it as
|
|
159
|
+
* present on every record and EQUAL to the block on the block's own tree. So
|
|
160
|
+
* a Button lookup printed "Button" in the via column of all 24 of its rows —
|
|
161
|
+
* a cell that is only ever news when it differs. Blank says "this block's
|
|
162
|
+
* own", which is the reading, and leaves the column carrying only the
|
|
163
|
+
* inherited trees.
|
|
164
|
+
*
|
|
165
|
+
* SPARQL lane only: the generated sub-SELECT can bind the entity's own name
|
|
166
|
+
* and filter against it, and a GraphQL document cannot.
|
|
167
|
+
*/
|
|
168
|
+
readonly blankWhenSelf?: true;
|
|
68
169
|
}
|
|
69
170
|
|
|
70
171
|
/**
|
|
@@ -119,6 +220,72 @@ export interface PackExpand {
|
|
|
119
220
|
readonly showWhenEmpty?: boolean;
|
|
120
221
|
/** Minimum canonical disclosure level at which this expand is fetched. */
|
|
121
222
|
readonly level?: string;
|
|
223
|
+
/**
|
|
224
|
+
* One sentence rendered under the section's heading, above the rows: how to
|
|
225
|
+
* READ them, when the columns do not say it themselves.
|
|
226
|
+
*
|
|
227
|
+
* The same seam a {@link PackSection} carries, for the same reason and
|
|
228
|
+
* through the same renderer field — a section's note was declarable only for
|
|
229
|
+
* a section whose body is one literal, and a TABLE can be the notation that
|
|
230
|
+
* needs reading just as much: a `rank` column is a position in a fallback
|
|
231
|
+
* chain, and nothing in the word "rank" says which chain or that first wins.
|
|
232
|
+
*/
|
|
233
|
+
readonly note?: string;
|
|
234
|
+
/**
|
|
235
|
+
* Child field names to ORDER BY in the generated sub-SELECT, in order.
|
|
236
|
+
*
|
|
237
|
+
* Without one an expand's rows arrive in the store's own scan order, which
|
|
238
|
+
* SPARQL does not define and which no reader can predict. That is tolerable
|
|
239
|
+
* for a short list of names and wrong for a TABLE whose rows differ in one
|
|
240
|
+
* column: Button's 24 token bindings came back with `spacing.internal.inline.start`
|
|
241
|
+
* and `…end` adjacent, in no stated order, and a reader comparing two rows
|
|
242
|
+
* had no way to know whether the pair was two facts or one repeated.
|
|
243
|
+
*
|
|
244
|
+
* Names must be fields the `select` declares (the grammar refuses others),
|
|
245
|
+
* so the clause is composed only from validated identifiers. SPARQL lane
|
|
246
|
+
* only — a GraphQL connection's order is the schema's, not the story's.
|
|
247
|
+
*/
|
|
248
|
+
readonly orderBy?: readonly string[];
|
|
249
|
+
/**
|
|
250
|
+
* Resolve THIS expand through the SPARQL lane even when the lookup is
|
|
251
|
+
* `source: "graphql"`.
|
|
252
|
+
*
|
|
253
|
+
* The lanes are not interchangeable, and a lookup is one or the other: the
|
|
254
|
+
* GraphQL lane can project a grandchild through a MULTI-valued relation
|
|
255
|
+
* (a nested expand), which the single-hop SPARQL sub-SELECT cannot, and the
|
|
256
|
+
* SPARQL lane can read property paths and any property the ontology defines,
|
|
257
|
+
* which the GraphQL lane cannot — a derived field name that the compiled
|
|
258
|
+
* schema does not carry drops out of the document in silence.
|
|
259
|
+
*
|
|
260
|
+
* The block lookup needs both at once, which is why this exists rather than
|
|
261
|
+
* a second lookup. Its modifier families are a nested expand and so must
|
|
262
|
+
* stay on the GraphQL lane; its token bindings must not, because
|
|
263
|
+
* `anatomy:styleKey` and `anatomy:styleState` — two of the six columns that
|
|
264
|
+
* IDENTIFY a binding — carry no `rdfs:domain`, so ke-graphql generates no
|
|
265
|
+
* field for them on `TokenBinding` and the two columns vanished without a
|
|
266
|
+
* word, leaving rows that differed only in a dropped column looking like
|
|
267
|
+
* duplicates.
|
|
268
|
+
*
|
|
269
|
+
* Only `"sparql"` is admissible: the SPARQL lane is every lookup's default,
|
|
270
|
+
* so an expand opting INTO GraphQL would be asking for a document the lookup
|
|
271
|
+
* never builds.
|
|
272
|
+
*/
|
|
273
|
+
readonly source?: "sparql";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Which lane resolves one expand: its own declared {@link PackExpand.source},
|
|
278
|
+
* else the lookup's, else SPARQL.
|
|
279
|
+
*
|
|
280
|
+
* Read by BOTH lanes — the GraphQL document generator skips what it does not
|
|
281
|
+
* own, and the entity fetcher runs it — so the question is answered in one
|
|
282
|
+
* place rather than asked twice with two spellings.
|
|
283
|
+
*/
|
|
284
|
+
export function expandIsSparql(
|
|
285
|
+
lookup: Pick<PackLookup, "source">,
|
|
286
|
+
expand: Pick<PackExpand, "source">,
|
|
287
|
+
): boolean {
|
|
288
|
+
return (expand.source ?? lookup.source ?? "sparql") === "sparql";
|
|
122
289
|
}
|
|
123
290
|
|
|
124
291
|
/**
|
|
@@ -136,8 +303,12 @@ export interface PackDisclosure {
|
|
|
136
303
|
|
|
137
304
|
/**
|
|
138
305
|
* A declarative list filter: a CLI/MCP parameter constraining one SELECT
|
|
139
|
-
* variable. Filters
|
|
140
|
-
* query text is never modified,
|
|
306
|
+
* variable. Filters compile INTO the generated query, as predicates over the
|
|
307
|
+
* author query read as a sub-select — the author's text is never modified, and
|
|
308
|
+
* a caller's value is bound through a `VALUES` block rather than spliced into
|
|
309
|
+
* query text, so filter input still cannot inject SPARQL. Compiling them in is
|
|
310
|
+
* what puts filtering on the right side of a page: a predicate over returned
|
|
311
|
+
* rows would decide over whatever the page happened to contain.
|
|
141
312
|
*/
|
|
142
313
|
export interface PackFilter {
|
|
143
314
|
/** Parameter name — a single lowercase word (`--param` / MCP key). */
|
|
@@ -161,11 +332,16 @@ export interface PackFilter {
|
|
|
161
332
|
* the hierarchy, also a `testing` standard, and a filter that compared only
|
|
162
333
|
* the leaf answered `--category testing` with 1 of 8 — a silently wrong
|
|
163
334
|
* answer, exit 0.
|
|
335
|
+
*
|
|
336
|
+
* A `"set"` cell is computed by the author's own `GROUP BY`, so its predicate
|
|
337
|
+
* can only run AFTER aggregation. That is why a filtered list wraps the
|
|
338
|
+
* author query in a sub-select rather than appending to it, and why a
|
|
339
|
+
* filterable story must project its variables by name.
|
|
164
340
|
*/
|
|
165
341
|
readonly match?: "exact" | "set";
|
|
166
342
|
/**
|
|
167
|
-
* Where the dimension's VOCABULARY lives in the graph
|
|
168
|
-
* declared {@link values}.
|
|
343
|
+
* Where the dimension's VOCABULARY lives in the graph. REQUIRED for a filter
|
|
344
|
+
* with no declared {@link values} — a filter declaring neither is refused.
|
|
169
345
|
*
|
|
170
346
|
* A value-free filter rejects a value the data does not carry. The question is
|
|
171
347
|
* what "the data" means. The rows a list just returned are the wrong answer:
|
|
@@ -180,9 +356,13 @@ export interface PackFilter {
|
|
|
180
356
|
* this honours is "the graph is the vocabulary, don't hard-code the slugs" —
|
|
181
357
|
* rows were never the graph, just the part of it that answered.
|
|
182
358
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
359
|
+
* A value-free filter USED to fall back to the observed rows — knowingly
|
|
360
|
+
* narrower than the truth, but the only evidence available. Now that a list
|
|
361
|
+
* answers with a PAGE that fallback is not merely narrow, it is wrong: a real
|
|
362
|
+
* value outside the window would be refused as a bad argument, with a
|
|
363
|
+
* truncated list of alternatives. So the fallback is gone and the declaration
|
|
364
|
+
* takes its place — a filter names `values` or a vocabulary, and one that
|
|
365
|
+
* names neither is refused where it is declared.
|
|
186
366
|
*/
|
|
187
367
|
readonly vocabulary?: PackFilterVocabulary;
|
|
188
368
|
/** Help text (defaults to a generated description). */
|
|
@@ -202,8 +382,9 @@ export interface PackFilterVocabulary {
|
|
|
202
382
|
|
|
203
383
|
/**
|
|
204
384
|
* Free-text search over list rows: a `--search` string keeping a row when ANY
|
|
205
|
-
* named SELECT variable contains the term (case-insensitive substring).
|
|
206
|
-
*
|
|
385
|
+
* named SELECT variable contains the term (case-insensitive substring).
|
|
386
|
+
* Compiled into the generated query alongside the filters, with the term bound
|
|
387
|
+
* through a `VALUES` block rather than spliced into query text.
|
|
207
388
|
*/
|
|
208
389
|
export interface PackSearch {
|
|
209
390
|
/** SELECT variables searched (without `?`). */
|
|
@@ -426,6 +607,73 @@ export interface PackLookup {
|
|
|
426
607
|
/** A pack list row / flat lookup base: variable name → string value. */
|
|
427
608
|
export type PackRow = Record<string, string>;
|
|
428
609
|
|
|
610
|
+
/**
|
|
611
|
+
* What a list-shaped verb answers with: one page of rows, and whether more
|
|
612
|
+
* follow.
|
|
613
|
+
*
|
|
614
|
+
* The ROWS are still the payload — `formatters.json` projects this to
|
|
615
|
+
* {@link rows} alone, so `--format json` and the MCP tool result stay the bare
|
|
616
|
+
* array they always were and nothing downstream had to learn a new shape. The
|
|
617
|
+
* page's own facts ride the `notice` seam into the envelope's `meta`, the same
|
|
618
|
+
* channel a calm empty result already uses. That is deliberate: a caller
|
|
619
|
+
* reading the data reads data, and a caller who needs to know there is more is
|
|
620
|
+
* told in the one place this surface already puts things the data cannot say
|
|
621
|
+
* about itself.
|
|
622
|
+
*/
|
|
623
|
+
export interface PackPage {
|
|
624
|
+
/** This page's rows, in the story's own order. */
|
|
625
|
+
readonly rows: readonly PackRow[];
|
|
626
|
+
/**
|
|
627
|
+
* The filters this read was narrowed by, as the caller spelled them — absent
|
|
628
|
+
* when the read was unfiltered.
|
|
629
|
+
*
|
|
630
|
+
* Carried because a zero-row answer has something to say that its rows
|
|
631
|
+
* cannot: which arguments narrowed it. A reader who typed `--kind input` and
|
|
632
|
+
* got nothing has to see that it was applied and came back empty, so the
|
|
633
|
+
* empty-state sentence names the filters in force. The story's
|
|
634
|
+
* `emptyRecovery` — which speaks about the population, and is worded to hold
|
|
635
|
+
* whether or not a filter is in force — prints under that sentence, the same
|
|
636
|
+
* as it prints on its own when nothing narrowed the read. The renderer cannot
|
|
637
|
+
* re-derive this: it sees a page, not the arguments, so the run body records
|
|
638
|
+
* what it narrowed by.
|
|
639
|
+
*/
|
|
640
|
+
readonly filters?: readonly PackAppliedFilter[];
|
|
641
|
+
/**
|
|
642
|
+
* The cursor that asks for the rows after this page, absent when this page is
|
|
643
|
+
* the last. Its presence IS the "more rows exist" answer.
|
|
644
|
+
*/
|
|
645
|
+
readonly nextAfter?: string;
|
|
646
|
+
/** The limit this page was cut to, for the notice that reports it. */
|
|
647
|
+
readonly limit: number;
|
|
648
|
+
/**
|
|
649
|
+
* The tier scope this read was narrowed to, absent when the story is not
|
|
650
|
+
* tiered or the caller asked for every tier.
|
|
651
|
+
*
|
|
652
|
+
* Carried for the same reason {@link filters} is: the rows cannot say that a
|
|
653
|
+
* whole part of the population was never considered. A reader who sees 175
|
|
654
|
+
* blocks under a heading that says `(175)` reads it as the design system; the
|
|
655
|
+
* scope is how the answer admits it is one tier chain of it.
|
|
656
|
+
*/
|
|
657
|
+
readonly scope?: PageTierScope;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** The tier scope a page was read under, as a caller can read it back. */
|
|
661
|
+
export interface PageTierScope {
|
|
662
|
+
/**
|
|
663
|
+
* The in-scope tiers, in the spelling the `--tier` flag and the `Tier` column
|
|
664
|
+
* use (the tier IRI's local name), base first and then shallowest first.
|
|
665
|
+
*/
|
|
666
|
+
readonly tiers: readonly string[];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** One filter a list read was narrowed by, as the caller spelled it. */
|
|
670
|
+
export interface PackAppliedFilter {
|
|
671
|
+
/** The parameter name (its CLI flag is `--<param>`). */
|
|
672
|
+
readonly param: string;
|
|
673
|
+
/** The value(s) supplied, already joined for display. */
|
|
674
|
+
readonly value: string;
|
|
675
|
+
}
|
|
676
|
+
|
|
429
677
|
/**
|
|
430
678
|
* A child record under an expand. Scalar values come from child fields; a nested
|
|
431
679
|
* expand contributes either a collapsed string array (single-field select) or
|
|
@@ -467,6 +715,60 @@ export interface PackDefinition {
|
|
|
467
715
|
* modules) by `pragma colophon`, after pragma's own built-in colophon.
|
|
468
716
|
*/
|
|
469
717
|
readonly colophon?: string;
|
|
718
|
+
/**
|
|
719
|
+
* The tier hierarchy this noun's entities carry, which SCOPES every read of
|
|
720
|
+
* them. Absent means unscoped — the read answers from every tier, as every
|
|
721
|
+
* read did before the scope existed.
|
|
722
|
+
*/
|
|
723
|
+
readonly tierScope?: PackTierScope;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* The tier hierarchy this noun's entities belong to, and the scope every read
|
|
728
|
+
* of them is narrowed by.
|
|
729
|
+
*
|
|
730
|
+
* DECLARED PER NOUN, because being tiered is a property of the entities: the
|
|
731
|
+
* design system's blocks, modifier families and concepts carry a `ds:tier`, and
|
|
732
|
+
* its token symbols, variables and code standards carry nothing of the kind. A
|
|
733
|
+
* noun that declares this gets the scope compiled into every list it declares
|
|
734
|
+
* and applied to its lookup; a noun that does not is read exactly as before.
|
|
735
|
+
*
|
|
736
|
+
* WHY THE HIERARCHY IS A NAME AND NOT AN EDGE. The shipped graph asserts no
|
|
737
|
+
* parent predicate on a tier — `ds:apps_lxd` is `a ds:Tier` with a
|
|
738
|
+
* `ds:name "Apps/LXD"`, and that slash IS the hierarchy: `Apps/LXD` sits under
|
|
739
|
+
* `Apps`, which sits under nothing. So the parent of a tier is the tier whose
|
|
740
|
+
* name is its own name minus the last `/` segment, exactly the derivation
|
|
741
|
+
* {@link PackScopeWeight} already ranks by ("the depth is the `/` count in the
|
|
742
|
+
* tier's OWN `ds:name`, not in its IRI"). Reading the hierarchy from the same
|
|
743
|
+
* place twice is what keeps the ranking and the scope from disagreeing about
|
|
744
|
+
* which tier is above which. The day a parent edge is asserted upstream, `by`
|
|
745
|
+
* gives way to it and every reader here changes in one place.
|
|
746
|
+
*/
|
|
747
|
+
export interface PackTierScope {
|
|
748
|
+
/** The class whose instances ARE the tiers (e.g. `ds:Tier`). */
|
|
749
|
+
readonly type: string;
|
|
750
|
+
/** The entity → tier edge (e.g. `ds:tier`), same term as `scopeWeight.via`. */
|
|
751
|
+
readonly via: string;
|
|
752
|
+
/**
|
|
753
|
+
* The tier property holding its path-shaped name (e.g. `ds:name`), whose
|
|
754
|
+
* `/`-separated segments ARE the hierarchy: `Apps/LXD` is a child of `Apps`,
|
|
755
|
+
* and a name with no `/` is top-level.
|
|
756
|
+
*/
|
|
757
|
+
readonly by: string;
|
|
758
|
+
/**
|
|
759
|
+
* The tier every other tier builds on, in scope whenever a scope is in force
|
|
760
|
+
* (e.g. `ds:global`).
|
|
761
|
+
*
|
|
762
|
+
* DECLARED, not inferred: nothing in the graph says which of the top-level
|
|
763
|
+
* tiers is the base — the design system's own colophon does ("`global` >
|
|
764
|
+
* `apps` > `apps/lxd`: a lower tier inherits and overrides the blocks of its
|
|
765
|
+
* ancestors"), and that is editorial judgement the ontology has not made yet,
|
|
766
|
+
* so it belongs in the declaration layer beside `weights` and `scopeWeight`.
|
|
767
|
+
* Without it a narrowed read would hide the very blocks the narrow tier
|
|
768
|
+
* inherits: `--tier apps_lxd` would answer with LXD's nine and not the 119
|
|
769
|
+
* global ones every LXD screen is actually built from.
|
|
770
|
+
*/
|
|
771
|
+
readonly base?: string;
|
|
470
772
|
}
|
|
471
773
|
|
|
472
774
|
/** A validated pack definition paired with where it was declared. */
|
|
@@ -175,7 +175,23 @@ function attachVerb(
|
|
|
175
175
|
|
|
176
176
|
command.action(async (...actionArgs: unknown[]) => {
|
|
177
177
|
const { positionals: positionalArgs, opts } = splitActionArgs(actionArgs);
|
|
178
|
-
|
|
178
|
+
// `command.args` is every operand this leaf received; the action arguments
|
|
179
|
+
// are only the DECLARED ones, so a verb that declares none is handed
|
|
180
|
+
// nothing and a stray word vanishes between the two. Dispatch reads the
|
|
181
|
+
// longer of the two lists and refuses what the verb cannot take
|
|
182
|
+
// (`dispatch.ts#refuseExcessPositionals`) — Commander's own excess check
|
|
183
|
+
// cannot: a sub-verb-only noun opts into excess operands to keep its
|
|
184
|
+
// "unknown command" suggestion, and `copyInheritedSettings` hands that
|
|
185
|
+
// opt-in down to every verb attached beneath it.
|
|
186
|
+
const operands = command.args.filter(
|
|
187
|
+
(arg): arg is string => typeof arg === "string",
|
|
188
|
+
);
|
|
189
|
+
await dispatch(
|
|
190
|
+
verb,
|
|
191
|
+
operands.length > positionalArgs.length ? operands : positionalArgs,
|
|
192
|
+
opts,
|
|
193
|
+
globalFlags,
|
|
194
|
+
);
|
|
179
195
|
});
|
|
180
196
|
}
|
|
181
197
|
|
|
@@ -379,6 +379,93 @@ describe("dispatch — errors", () => {
|
|
|
379
379
|
});
|
|
380
380
|
});
|
|
381
381
|
|
|
382
|
+
describe("a positional the spec cannot hold is refused, not dropped", () => {
|
|
383
|
+
const savedExit = process.exitCode;
|
|
384
|
+
afterEach(() => {
|
|
385
|
+
process.exitCode = savedExit;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
/** Run `verb` with `positionals` and collect what stderr was told. */
|
|
389
|
+
async function stderrOf(
|
|
390
|
+
verb: VerbSpec,
|
|
391
|
+
positionals: string[],
|
|
392
|
+
): Promise<string> {
|
|
393
|
+
const errs: string[] = [];
|
|
394
|
+
const spy = vi
|
|
395
|
+
.spyOn(process.stderr, "write")
|
|
396
|
+
.mockImplementation((chunk: string | Uint8Array) => {
|
|
397
|
+
errs.push(String(chunk));
|
|
398
|
+
return true;
|
|
399
|
+
});
|
|
400
|
+
// The verb that is NOT refused runs and prints; keep it out of the report.
|
|
401
|
+
const out = vi
|
|
402
|
+
.spyOn(process.stdout, "write")
|
|
403
|
+
.mockImplementation(() => true);
|
|
404
|
+
await dispatch(verb, positionals, {}, PLAIN);
|
|
405
|
+
spy.mockRestore();
|
|
406
|
+
out.mockRestore();
|
|
407
|
+
return errs.join("");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** A verb addressed by a flag, the shape `variable chain` has. */
|
|
411
|
+
const chain: VerbSpec = {
|
|
412
|
+
path: ["gizmo", "chain"],
|
|
413
|
+
summary: "Walk a gizmo.",
|
|
414
|
+
params: [
|
|
415
|
+
{ kind: "string", name: "gizmo", doc: "The gizmo." },
|
|
416
|
+
{ kind: "number", name: "limit", doc: "Rows." },
|
|
417
|
+
],
|
|
418
|
+
output: { formatters: passthroughFormatters },
|
|
419
|
+
capability: { needsStore: false, mutates: false, mcp: { expose: true } },
|
|
420
|
+
run: async () => ({ walked: true }),
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
it("names the flag the value belonged to, and exits 2", async () => {
|
|
424
|
+
const out = await stderrOf(chain, ["color-text"]);
|
|
425
|
+
|
|
426
|
+
expect(out).toContain(
|
|
427
|
+
'`pragma gizmo chain` takes no positional argument, but received "color-text".',
|
|
428
|
+
);
|
|
429
|
+
expect(out).toContain("Did you mean `--gizmo color-text`?");
|
|
430
|
+
expect(process.exitCode).toBe(2);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("points at --help when no flag could have held it", async () => {
|
|
434
|
+
const bare: VerbSpec = { ...chain, params: [] };
|
|
435
|
+
|
|
436
|
+
const out = await stderrOf(bare, ["color-text"]);
|
|
437
|
+
|
|
438
|
+
expect(out).toContain("takes no positional argument");
|
|
439
|
+
expect(out).toContain("run `pragma gizmo chain --help`");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("refuses one MORE than the spec declares", async () => {
|
|
443
|
+
const out = await stderrOf(echo, ["hello", "spare"]);
|
|
444
|
+
|
|
445
|
+
expect(out).toContain(
|
|
446
|
+
'`pragma probe echo` takes 1 positional argument, but received an extra "spare".',
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("leaves a variadic verb alone — its tail is the point", async () => {
|
|
451
|
+
const many: VerbSpec = {
|
|
452
|
+
...echo,
|
|
453
|
+
params: [
|
|
454
|
+
{
|
|
455
|
+
kind: "string[]",
|
|
456
|
+
name: "name",
|
|
457
|
+
doc: "Names.",
|
|
458
|
+
positional: true,
|
|
459
|
+
required: true,
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
run: async (p) => (p as { name: string[] }).name,
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
expect(await stderrOf(many, ["one", "two", "three"])).toBe("");
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
382
469
|
describe("--dry-run is honest (PRA-104)", () => {
|
|
383
470
|
const savedExit = process.exitCode;
|
|
384
471
|
afterEach(() => {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { describeEffect, type Effect, type Task } from "@canonical/task";
|
|
15
15
|
import { runPreview, runTask, runUndo } from "@canonical/task/node";
|
|
16
|
+
import { BIN_NAME } from "../../../constants.js";
|
|
16
17
|
import {
|
|
17
18
|
asPragmaError,
|
|
18
19
|
CANCELLED_MESSAGE,
|
|
@@ -34,7 +35,7 @@ import type {
|
|
|
34
35
|
InteractionRuntime,
|
|
35
36
|
PragmaRuntime,
|
|
36
37
|
} from "../../runtime/types.js";
|
|
37
|
-
import type
|
|
38
|
+
import { kebabCase, type ParamSpec, type VerbSpec } from "../../spec/index.js";
|
|
38
39
|
import { EXIT, mapExitCode } from "./exitCodes.js";
|
|
39
40
|
|
|
40
41
|
/** The CLI-only mutation flags auto-injected onto every mutating verb. */
|
|
@@ -177,9 +178,17 @@ function renderData(
|
|
|
177
178
|
// (`mcp/registerVerb.ts#noticeMeta`) — the two machine surfaces stay
|
|
178
179
|
// byte-equal.
|
|
179
180
|
const notice = verb.output.formatters.notice?.(data);
|
|
181
|
+
// The machine half of the same seam: facts the read wants to hand an agent
|
|
182
|
+
// as data rather than prose (`meta.scope`). MCP merges the same seam into
|
|
183
|
+
// the same keys (`mcp/registerVerb.ts#readMeta`).
|
|
184
|
+
const extra = verb.output.formatters.meta?.(data);
|
|
180
185
|
return {
|
|
181
186
|
stdout: `${JSON.stringify(
|
|
182
|
-
successEnvelope(projection,
|
|
187
|
+
successEnvelope(projection, {
|
|
188
|
+
...meta,
|
|
189
|
+
...(extra ?? {}),
|
|
190
|
+
...(notice ? { notice } : {}),
|
|
191
|
+
}),
|
|
183
192
|
)}\n`,
|
|
184
193
|
exitCode: 0,
|
|
185
194
|
};
|
|
@@ -411,7 +420,29 @@ export async function executeVerb(
|
|
|
411
420
|
}
|
|
412
421
|
}
|
|
413
422
|
if (mutation.undo) {
|
|
414
|
-
|
|
423
|
+
// The truth seam. `runUndo`'s collection phase drove the verb's task
|
|
424
|
+
// with every effect mocked, so anything the task body reported was a
|
|
425
|
+
// narration of the mocked walk, not of the reversals — which have only
|
|
426
|
+
// JUST run, each isolated, each with its own outcome. Those outcomes
|
|
427
|
+
// are the one honest record of what the undo did: a verb that reports
|
|
428
|
+
// per-target results sets `undoReport` (like `exec`/`planData`) and
|
|
429
|
+
// projects them onto its own rows; a throw from it renders as the
|
|
430
|
+
// run's error, exactly like a failed real run. Verbs without a
|
|
431
|
+
// projector of their own still may not exit 0 over a failed reversal,
|
|
432
|
+
// so the outcomes are re-checked here as the backstop.
|
|
433
|
+
const { undoCount, outcomes } = await runUndo(task, {
|
|
434
|
+
onLog: logSink(flags),
|
|
435
|
+
});
|
|
436
|
+
await mutationRuntime.undoReport?.(outcomes);
|
|
437
|
+
const failed = outcomes.filter((o) => o.status === "failed");
|
|
438
|
+
if (failed.length > 0) {
|
|
439
|
+
throw new PragmaError({
|
|
440
|
+
code: "UNSUPPORTED",
|
|
441
|
+
message: `${failed.length} of ${outcomes.length} undo step(s) did not complete: ${failed
|
|
442
|
+
.map((o) => o.error.message)
|
|
443
|
+
.join("; ")}`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
415
446
|
return renderUndo(flags, undoCount);
|
|
416
447
|
}
|
|
417
448
|
// Real execution: spread the verb's runner options into the node
|
|
@@ -508,12 +539,91 @@ export async function dispatch(
|
|
|
508
539
|
};
|
|
509
540
|
await runPrepared(
|
|
510
541
|
verb,
|
|
511
|
-
() =>
|
|
542
|
+
() => {
|
|
543
|
+
refuseExcessPositionals(verb, positionals);
|
|
544
|
+
return extractParams(verb.params, positionals, opts);
|
|
545
|
+
},
|
|
512
546
|
mutation,
|
|
513
547
|
globalFlags,
|
|
514
548
|
);
|
|
515
549
|
}
|
|
516
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Refuse a positional the verb's spec has nowhere to put.
|
|
553
|
+
*
|
|
554
|
+
* `extractParams` maps positionals onto DECLARED positional params and drops
|
|
555
|
+
* the rest, so `pragma variable chain color-text` — whose story takes its
|
|
556
|
+
* subject as `--variable` — ran the unfiltered chain and printed 300 unrelated
|
|
557
|
+
* rows, having silently discarded the one word the user cared about. A wrong
|
|
558
|
+
* answer with no diagnostic is worse than a refusal.
|
|
559
|
+
*
|
|
560
|
+
* The refusal names the flag the value most likely belongs to: the verb's own
|
|
561
|
+
* noun as a flag (`variable chain` → `--variable`), else its first
|
|
562
|
+
* value-taking flag. That is the identity filter for every story shaped like
|
|
563
|
+
* this one, and where it is not, the flag list is one `--help` away.
|
|
564
|
+
*
|
|
565
|
+
* @param verb - The verb the user invoked.
|
|
566
|
+
* @param positionals - The operands Commander collected for it.
|
|
567
|
+
* @throws PragmaError INVALID_INPUT when more operands arrived than the spec
|
|
568
|
+
* declares (a variadic positional absorbs the tail, so it never trips).
|
|
569
|
+
*/
|
|
570
|
+
function refuseExcessPositionals(
|
|
571
|
+
verb: VerbSpec,
|
|
572
|
+
positionals: readonly string[],
|
|
573
|
+
): void {
|
|
574
|
+
const declared = verb.params.filter((param) => param.positional);
|
|
575
|
+
if (declared.some((param) => param.kind === "string[]")) return;
|
|
576
|
+
const excess = positionals[declared.length];
|
|
577
|
+
if (excess === undefined) return;
|
|
578
|
+
const command = `${BIN_NAME} ${verb.path.join(" ")}`;
|
|
579
|
+
const flag = suggestedFlag(verb);
|
|
580
|
+
throw new PragmaError({
|
|
581
|
+
code: "INVALID_INPUT",
|
|
582
|
+
message:
|
|
583
|
+
declared.length === 0
|
|
584
|
+
? `\`${command}\` takes no positional argument, but received "${excess}".`
|
|
585
|
+
: `\`${command}\` takes ${declared.length} positional argument${
|
|
586
|
+
declared.length === 1 ? "" : "s"
|
|
587
|
+
}, but received an extra "${excess}".`,
|
|
588
|
+
recovery: {
|
|
589
|
+
message: flag
|
|
590
|
+
? `Did you mean \`--${flag} ${excess}\`?`
|
|
591
|
+
: `Pass values as flags — run \`${command} --help\` to see them.`,
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* The flag a stray positional most likely belonged to.
|
|
598
|
+
*
|
|
599
|
+
* The verb's NOUN first (`variable chain` declares `--variable`, which is the
|
|
600
|
+
* identity filter every such story is addressed by), then the first flag that
|
|
601
|
+
* takes a value at all. Paging and disclosure flags are never the answer — a
|
|
602
|
+
* bare word is not a cursor.
|
|
603
|
+
*
|
|
604
|
+
* @param verb - The verb the user invoked.
|
|
605
|
+
* @returns The flag name (without dashes), or undefined when none fits.
|
|
606
|
+
*/
|
|
607
|
+
function suggestedFlag(verb: VerbSpec): string | undefined {
|
|
608
|
+
const candidates = verb.params.filter(
|
|
609
|
+
(param) =>
|
|
610
|
+
!param.positional &&
|
|
611
|
+
param.kind !== "boolean" &&
|
|
612
|
+
!GENERIC_FLAGS.has(param.name),
|
|
613
|
+
);
|
|
614
|
+
const identity = candidates.find((param) => param.name === verb.path[0]);
|
|
615
|
+
const flag = identity ?? candidates[0];
|
|
616
|
+
return flag ? kebabCase(flag.name) : undefined;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Flags that answer no question a stray word could be the answer to. */
|
|
620
|
+
const GENERIC_FLAGS: ReadonlySet<string> = new Set([
|
|
621
|
+
"limit",
|
|
622
|
+
"after",
|
|
623
|
+
"detail",
|
|
624
|
+
"count",
|
|
625
|
+
]);
|
|
626
|
+
|
|
517
627
|
/**
|
|
518
628
|
* Dispatch a verb whose params were ALREADY extracted by the caller — the
|
|
519
629
|
* seam a mounted subtree drives: its leaf specs carry no Commander defaults
|