diff --git a/.tsbuildcache/node.tsbuildinfo b/.tsbuildcache/node.tsbuildinfo new file mode 100644 index 00000000..eeccfb85 --- /dev/null +++ b/.tsbuildcache/node.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["../node_modules/typescript/lib/lib.es5.d.ts","../node_modules/typescript/lib/lib.es2015.d.ts","../node_modules/typescript/lib/lib.es2016.d.ts","../node_modules/typescript/lib/lib.es2017.d.ts","../node_modules/typescript/lib/lib.es2018.d.ts","../node_modules/typescript/lib/lib.es2019.d.ts","../node_modules/typescript/lib/lib.es2020.d.ts","../node_modules/typescript/lib/lib.es2021.d.ts","../node_modules/typescript/lib/lib.es2022.d.ts","../node_modules/typescript/lib/lib.es2015.core.d.ts","../node_modules/typescript/lib/lib.es2015.collection.d.ts","../node_modules/typescript/lib/lib.es2015.generator.d.ts","../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../node_modules/typescript/lib/lib.es2015.promise.d.ts","../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../node_modules/typescript/lib/lib.es2016.intl.d.ts","../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../node_modules/typescript/lib/lib.es2017.date.d.ts","../node_modules/typescript/lib/lib.es2017.object.d.ts","../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2017.string.d.ts","../node_modules/typescript/lib/lib.es2017.intl.d.ts","../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../node_modules/typescript/lib/lib.es2018.intl.d.ts","../node_modules/typescript/lib/lib.es2018.promise.d.ts","../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../node_modules/typescript/lib/lib.es2019.array.d.ts","../node_modules/typescript/lib/lib.es2019.object.d.ts","../node_modules/typescript/lib/lib.es2019.string.d.ts","../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../node_modules/typescript/lib/lib.es2019.intl.d.ts","../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../node_modules/typescript/lib/lib.es2020.date.d.ts","../node_modules/typescript/lib/lib.es2020.promise.d.ts","../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2020.string.d.ts","../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2020.intl.d.ts","../node_modules/typescript/lib/lib.es2020.number.d.ts","../node_modules/typescript/lib/lib.es2021.promise.d.ts","../node_modules/typescript/lib/lib.es2021.string.d.ts","../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../node_modules/typescript/lib/lib.es2021.intl.d.ts","../node_modules/typescript/lib/lib.es2022.array.d.ts","../node_modules/typescript/lib/lib.es2022.error.d.ts","../node_modules/typescript/lib/lib.es2022.intl.d.ts","../node_modules/typescript/lib/lib.es2022.object.d.ts","../node_modules/typescript/lib/lib.es2022.string.d.ts","../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../node_modules/typescript/lib/lib.es2025.float16.d.ts","../node_modules/typescript/lib/lib.esnext.disposable.d.ts","../node_modules/typescript/lib/lib.decorators.d.ts","../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../node_modules/@types/node/globals.typedarray.d.ts","../node_modules/@types/node/buffer.buffer.d.ts","../node_modules/@types/node/globals.d.ts","../node_modules/@types/node/web-globals/abortcontroller.d.ts","../node_modules/@types/node/web-globals/blob.d.ts","../node_modules/@types/node/web-globals/console.d.ts","../node_modules/@types/node/web-globals/crypto.d.ts","../node_modules/@types/node/web-globals/domexception.d.ts","../node_modules/@types/node/web-globals/encoding.d.ts","../node_modules/@types/node/web-globals/events.d.ts","../node_modules/undici-types/utility.d.ts","../node_modules/undici-types/header.d.ts","../node_modules/undici-types/readable.d.ts","../node_modules/undici-types/fetch.d.ts","../node_modules/undici-types/formdata.d.ts","../node_modules/undici-types/connector.d.ts","../node_modules/undici-types/client-stats.d.ts","../node_modules/undici-types/client.d.ts","../node_modules/undici-types/errors.d.ts","../node_modules/undici-types/dispatcher.d.ts","../node_modules/undici-types/global-dispatcher.d.ts","../node_modules/undici-types/global-origin.d.ts","../node_modules/undici-types/pool-stats.d.ts","../node_modules/undici-types/pool.d.ts","../node_modules/undici-types/handlers.d.ts","../node_modules/undici-types/balanced-pool.d.ts","../node_modules/undici-types/round-robin-pool.d.ts","../node_modules/undici-types/h2c-client.d.ts","../node_modules/undici-types/agent.d.ts","../node_modules/undici-types/dispatcher1-wrapper.d.ts","../node_modules/undici-types/mock-interceptor.d.ts","../node_modules/undici-types/mock-call-history.d.ts","../node_modules/undici-types/mock-agent.d.ts","../node_modules/undici-types/mock-client.d.ts","../node_modules/undici-types/mock-pool.d.ts","../node_modules/undici-types/snapshot-agent.d.ts","../node_modules/undici-types/mock-errors.d.ts","../node_modules/undici-types/proxy-agent.d.ts","../node_modules/undici-types/socks5-proxy-agent.d.ts","../node_modules/undici-types/env-http-proxy-agent.d.ts","../node_modules/undici-types/retry-handler.d.ts","../node_modules/undici-types/retry-agent.d.ts","../node_modules/undici-types/api.d.ts","../node_modules/undici-types/cache-interceptor.d.ts","../node_modules/undici-types/interceptors.d.ts","../node_modules/undici-types/util.d.ts","../node_modules/undici-types/cookies.d.ts","../node_modules/undici-types/patch.d.ts","../node_modules/undici-types/websocket.d.ts","../node_modules/undici-types/eventsource.d.ts","../node_modules/undici-types/diagnostics-channel.d.ts","../node_modules/undici-types/content-type.d.ts","../node_modules/undici-types/cache.d.ts","../node_modules/undici-types/index.d.ts","../node_modules/@types/node/web-globals/fetch.d.ts","../node_modules/@types/node/web-globals/importmeta.d.ts","../node_modules/@types/node/web-globals/messaging.d.ts","../node_modules/@types/node/web-globals/navigator.d.ts","../node_modules/@types/node/web-globals/performance.d.ts","../node_modules/@types/node/web-globals/storage.d.ts","../node_modules/@types/node/web-globals/streams.d.ts","../node_modules/@types/node/web-globals/timers.d.ts","../node_modules/@types/node/web-globals/url.d.ts","../node_modules/@types/node/assert.d.ts","../node_modules/@types/node/assert/strict.d.ts","../node_modules/@types/node/async_hooks.d.ts","../node_modules/@types/node/buffer.d.ts","../node_modules/@types/node/child_process.d.ts","../node_modules/@types/node/cluster.d.ts","../node_modules/@types/node/console.d.ts","../node_modules/@types/node/constants.d.ts","../node_modules/@types/node/crypto.d.ts","../node_modules/@types/node/dgram.d.ts","../node_modules/@types/node/diagnostics_channel.d.ts","../node_modules/@types/node/dns.d.ts","../node_modules/@types/node/dns/promises.d.ts","../node_modules/@types/node/domain.d.ts","../node_modules/@types/node/events.d.ts","../node_modules/@types/node/ffi.d.ts","../node_modules/@types/node/fs.d.ts","../node_modules/@types/node/fs/promises.d.ts","../node_modules/@types/node/http.d.ts","../node_modules/@types/node/http2.d.ts","../node_modules/@types/node/https.d.ts","../node_modules/@types/node/inspector.d.ts","../node_modules/@types/node/inspector.generated.d.ts","../node_modules/@types/node/inspector/promises.d.ts","../node_modules/@types/node/module.d.ts","../node_modules/@types/node/net.d.ts","../node_modules/@types/node/os.d.ts","../node_modules/@types/node/path.d.ts","../node_modules/@types/node/path/posix.d.ts","../node_modules/@types/node/path/win32.d.ts","../node_modules/@types/node/perf_hooks.d.ts","../node_modules/@types/node/process.d.ts","../node_modules/@types/node/punycode.d.ts","../node_modules/@types/node/querystring.d.ts","../node_modules/@types/node/quic.d.ts","../node_modules/@types/node/readline.d.ts","../node_modules/@types/node/readline/promises.d.ts","../node_modules/@types/node/repl.d.ts","../node_modules/@types/node/sea.d.ts","../node_modules/@types/node/sqlite.d.ts","../node_modules/@types/node/stream.d.ts","../node_modules/@types/node/stream/consumers.d.ts","../node_modules/@types/node/stream/iter.d.ts","../node_modules/@types/node/stream/promises.d.ts","../node_modules/@types/node/stream/web.d.ts","../node_modules/@types/node/string_decoder.d.ts","../node_modules/@types/node/test.d.ts","../node_modules/@types/node/test/reporters.d.ts","../node_modules/@types/node/timers.d.ts","../node_modules/@types/node/timers/promises.d.ts","../node_modules/@types/node/tls.d.ts","../node_modules/@types/node/trace_events.d.ts","../node_modules/@types/node/tty.d.ts","../node_modules/@types/node/url.d.ts","../node_modules/@types/node/util.d.ts","../node_modules/@types/node/util/types.d.ts","../node_modules/@types/node/v8.d.ts","../node_modules/@types/node/vm.d.ts","../node_modules/@types/node/wasi.d.ts","../node_modules/@types/node/worker_threads.d.ts","../node_modules/@types/node/zlib.d.ts","../node_modules/@types/node/zlib/iter.d.ts","../node_modules/@types/node/index.d.ts","../node_modules/vite/types/hmrpayload.d.ts","../node_modules/vite/dist/node/chunks/modulerunnertransport.d.ts","../node_modules/vite/types/customevent.d.ts","../node_modules/rolldown/dist/shared/logging-xuho4may.d.mts","../node_modules/@oxc-project/types/types.d.ts","../node_modules/rolldown/dist/shared/binding-cvtkjvyl.d.mts","../node_modules/@rolldown/pluginutils/dist/filter/index.d.mts","../node_modules/@rolldown/pluginutils/dist/index.d.mts","../node_modules/rolldown/dist/shared/define-config-dsmnxceb.d.mts","../node_modules/rolldown/dist/index.d.mts","../node_modules/rolldown/dist/parse-ast-index.d.mts","../node_modules/vite/types/internal/rolluptypecompat.d.ts","../node_modules/rolldown/dist/shared/constructors-alilxaii.d.mts","../node_modules/rolldown/dist/plugins-index.d.mts","../node_modules/rolldown/dist/shared/transform-wmu_ci8e.d.mts","../node_modules/rolldown/dist/utils-index.d.mts","../node_modules/vite/types/hot.d.ts","../node_modules/vite/dist/node/module-runner.d.ts","../node_modules/vite/types/internal/esbuildoptions.d.ts","../node_modules/vite/types/metadata.d.ts","../node_modules/vite/types/internal/terseroptions.d.ts","../node_modules/source-map-js/source-map.d.ts","../node_modules/postcss/lib/previous-map.d.ts","../node_modules/postcss/lib/input.d.ts","../node_modules/postcss/lib/css-syntax-error.d.ts","../node_modules/postcss/lib/declaration.d.ts","../node_modules/postcss/lib/root.d.ts","../node_modules/postcss/lib/warning.d.ts","../node_modules/postcss/lib/lazy-result.d.ts","../node_modules/postcss/lib/no-work-result.d.ts","../node_modules/postcss/lib/processor.d.ts","../node_modules/postcss/lib/result.d.ts","../node_modules/postcss/lib/document.d.ts","../node_modules/postcss/lib/rule.d.ts","../node_modules/postcss/lib/node.d.ts","../node_modules/postcss/lib/comment.d.ts","../node_modules/postcss/lib/container.d.ts","../node_modules/postcss/lib/at-rule.d.ts","../node_modules/postcss/lib/list.d.ts","../node_modules/postcss/lib/postcss.d.ts","../node_modules/postcss/lib/postcss.d.mts","../node_modules/vite/node_modules/lightningcss/node/ast.d.ts","../node_modules/vite/node_modules/lightningcss/node/targets.d.ts","../node_modules/vite/node_modules/lightningcss/node/index.d.ts","../node_modules/vite/types/internal/lightningcssoptions.d.ts","../node_modules/vite/types/internal/csspreprocessoroptions.d.ts","../node_modules/rolldown/dist/filter-index.d.mts","../node_modules/vite/types/importglob.d.ts","../node_modules/vite/dist/node/index.d.ts","../node_modules/@vitejs/plugin-react/types/optionaltypes.d.ts","../node_modules/@vitejs/plugin-react/dist/index.d.ts","../node_modules/simple-git/dist/src/lib/tasks/diff-name-status.d.ts","../node_modules/simple-git/dist/src/lib/tasks/task.d.ts","../node_modules/simple-git/dist/src/lib/types/tasks.d.ts","../node_modules/simple-git/dist/src/lib/errors/git-error.d.ts","../node_modules/simple-git/dist/src/lib/types/handlers.d.ts","../node_modules/@simple-git/argv-parser/dist/src/vulnerabilities/vulnerability.types.d.ts","../node_modules/@simple-git/argv-parser/dist/src/args/parse-argv.types.d.ts","../node_modules/@simple-git/argv-parser/dist/src/args/parse-argv.d.ts","../node_modules/@simple-git/argv-parser/dist/src/env/parse-env.d.ts","../node_modules/@simple-git/argv-parser/dist/src/vulnerabilities/vulnerability-check.d.ts","../node_modules/@simple-git/argv-parser/dist/index.d.ts","../node_modules/simple-git/dist/src/lib/types/index.d.ts","../node_modules/simple-git/dist/src/lib/tasks/log.d.ts","../node_modules/simple-git/dist/typings/response.d.ts","../node_modules/simple-git/dist/src/lib/responses/getremotesummary.d.ts","../node_modules/simple-git/dist/src/lib/tasks/apply-patch.d.ts","../node_modules/simple-git/dist/src/lib/tasks/check-is-repo.d.ts","../node_modules/simple-git/dist/src/lib/tasks/clean.d.ts","../node_modules/simple-git/dist/src/lib/tasks/clone.d.ts","../node_modules/simple-git/dist/src/lib/tasks/config.d.ts","../node_modules/simple-git/dist/src/lib/tasks/count-objects.d.ts","../node_modules/simple-git/dist/src/lib/tasks/grep.d.ts","../node_modules/simple-git/dist/src/lib/tasks/reset.d.ts","../node_modules/simple-git/dist/src/lib/tasks/version.d.ts","../node_modules/simple-git/dist/typings/types.d.ts","../node_modules/simple-git/dist/src/lib/errors/git-construct-error.d.ts","../node_modules/simple-git/dist/src/lib/errors/git-plugin-error.d.ts","../node_modules/simple-git/dist/src/lib/errors/git-response-error.d.ts","../node_modules/simple-git/dist/src/lib/errors/task-configuration-error.d.ts","../node_modules/simple-git/dist/typings/errors.d.ts","../node_modules/simple-git/dist/typings/simple-git.d.ts","../node_modules/@simple-git/args-pathspec/dist/src/pathspec.d.ts","../node_modules/@simple-git/args-pathspec/dist/index.d.ts","../node_modules/simple-git/dist/typings/index.d.ts","../node_modules/fast-uri/types/index.d.ts","../node_modules/ajv/dist/compile/codegen/code.d.ts","../node_modules/ajv/dist/compile/codegen/scope.d.ts","../node_modules/ajv/dist/compile/codegen/index.d.ts","../node_modules/ajv/dist/compile/rules.d.ts","../node_modules/ajv/dist/compile/util.d.ts","../node_modules/ajv/dist/compile/validate/subschema.d.ts","../node_modules/ajv/dist/compile/errors.d.ts","../node_modules/ajv/dist/compile/validate/index.d.ts","../node_modules/ajv/dist/compile/validate/datatype.d.ts","../node_modules/ajv/dist/vocabularies/applicator/additionalitems.d.ts","../node_modules/ajv/dist/vocabularies/applicator/items2020.d.ts","../node_modules/ajv/dist/vocabularies/applicator/contains.d.ts","../node_modules/ajv/dist/vocabularies/applicator/dependencies.d.ts","../node_modules/ajv/dist/vocabularies/applicator/propertynames.d.ts","../node_modules/ajv/dist/vocabularies/applicator/additionalproperties.d.ts","../node_modules/ajv/dist/vocabularies/applicator/not.d.ts","../node_modules/ajv/dist/vocabularies/applicator/anyof.d.ts","../node_modules/ajv/dist/vocabularies/applicator/oneof.d.ts","../node_modules/ajv/dist/vocabularies/applicator/if.d.ts","../node_modules/ajv/dist/vocabularies/applicator/index.d.ts","../node_modules/ajv/dist/vocabularies/validation/limitnumber.d.ts","../node_modules/ajv/dist/vocabularies/validation/multipleof.d.ts","../node_modules/ajv/dist/vocabularies/validation/pattern.d.ts","../node_modules/ajv/dist/vocabularies/validation/required.d.ts","../node_modules/ajv/dist/vocabularies/validation/uniqueitems.d.ts","../node_modules/ajv/dist/vocabularies/validation/const.d.ts","../node_modules/ajv/dist/vocabularies/validation/enum.d.ts","../node_modules/ajv/dist/vocabularies/validation/index.d.ts","../node_modules/ajv/dist/vocabularies/format/format.d.ts","../node_modules/ajv/dist/vocabularies/unevaluated/unevaluatedproperties.d.ts","../node_modules/ajv/dist/vocabularies/unevaluated/unevaluateditems.d.ts","../node_modules/ajv/dist/vocabularies/validation/dependentrequired.d.ts","../node_modules/ajv/dist/vocabularies/discriminator/types.d.ts","../node_modules/ajv/dist/vocabularies/discriminator/index.d.ts","../node_modules/ajv/dist/vocabularies/errors.d.ts","../node_modules/ajv/dist/types/json-schema.d.ts","../node_modules/ajv/dist/types/jtd-schema.d.ts","../node_modules/ajv/dist/runtime/validation_error.d.ts","../node_modules/ajv/dist/compile/ref_error.d.ts","../node_modules/ajv/dist/core.d.ts","../node_modules/ajv/dist/compile/resolve.d.ts","../node_modules/ajv/dist/compile/index.d.ts","../node_modules/ajv/dist/types/index.d.ts","../node_modules/ajv/dist/ajv.d.ts","../node_modules/ajv-formats/dist/formats.d.ts","../node_modules/ajv-formats/dist/limit.d.ts","../node_modules/ajv-formats/dist/index.d.ts","../src/utils/validatedata.ts","../src/utils/loaddata.ts","../node_modules/anymatch/index.d.ts","../node_modules/chokidar/types/index.d.ts","../node_modules/vite-plugin-static-copy/dist/index.d.ts","../package.json","../node_modules/@types/react/global.d.ts","../node_modules/csstype/index.d.ts","../node_modules/@types/react/index.d.ts","../node_modules/@types/react/jsx-runtime.d.ts","../src/schema/pathwaymetadata.v1.json","../src/schema/common/publication.v1.json","../src/schema/common/sector.v1.json","../src/schema/common/technology.v1.json","../src/schema/common/metric.v1.json","../src/schema/common/label.v1.json","../src/schema/common/geographyitem.v1.json","../src/schema/common/countrycode.v1.json","../src/schema/common/geography.v1.json","../src/schema/common/emissionsscope.v1.json","../src/schema/common/scopesector.v2.json","../src/schema/common/scopegeography.v2.json","../src/schema/common/index.ts","../vite.config.ts","../node_modules/@types/estree/index.d.ts","../node_modules/@types/json-schema/index.d.ts","../node_modules/@eslint/core/dist/cjs/types.d.cts","../node_modules/@eslint/plugin-kit/dist/cjs/types.cts","../node_modules/@eslint/plugin-kit/dist/cjs/index.d.cts","../node_modules/eslint/lib/types/index.d.ts","../node_modules/@eslint/js/types/index.d.ts","../node_modules/@eslint-react/eslint-plugin/dist/index.d.ts","../node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.d.ts","../node_modules/eslint-plugin-react-hooks/index.d.ts","../node_modules/globals/index.d.ts","../node_modules/typescript/lib/typescript.d.ts","../node_modules/@typescript-eslint/types/dist/generated/ast-spec.d.ts","../node_modules/@typescript-eslint/types/dist/lib.d.ts","../node_modules/@typescript-eslint/types/dist/parser-options.d.ts","../node_modules/@typescript-eslint/types/dist/ts-estree.d.ts","../node_modules/@typescript-eslint/types/dist/index.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/clear-caches.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/create-program/getscriptkind.d.ts","../node_modules/typescript/lib/tsserverlibrary.d.ts","../node_modules/@typescript-eslint/project-service/dist/createprojectservice.d.ts","../node_modules/@typescript-eslint/project-service/dist/index.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/ts-estree/ts-nodes.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/ts-estree/estree-to-ts-node-types.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/ts-estree/index.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/parsesettings/expiringcache.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/parsesettings/index.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/create-program/shared.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/create-program/useprovidedprograms.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/getmodifiers.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/node-utils.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/parser-options.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/parser.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/parsesettings/candidatetsconfigrootdirs.d.ts","../node_modules/@typescript-eslint/visitor-keys/dist/get-keys.d.ts","../node_modules/@typescript-eslint/visitor-keys/dist/visitor-keys.d.ts","../node_modules/@typescript-eslint/visitor-keys/dist/index.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/simple-traverse.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/version-check.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/version.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/withoutprojectparseroptions.d.ts","../node_modules/@typescript-eslint/typescript-estree/dist/index.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-estree.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/ast.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/parseroptions.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/definitiontype.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/definitionbase.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/catchclausedefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/classnamedefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/functionnamedefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/implicitglobalvariabledefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/importbindingdefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/parameterdefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/tsenummemberdefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/tsenumnamedefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/tsmodulenamedefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/typedefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/variabledefinition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/definition.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/definition/index.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/referencer/reference.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/variable/variablebase.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/variable/eslintscopevariable.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/variable/variable.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/variable/implicitlibvariable.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/variable/index.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/scopetype.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/functionscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/globalscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/modulescope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/tsmodulescope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/scopebase.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/catchscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/classscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/classstaticblockscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/conditionaltypescope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/forscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/functionexpressionnamescope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/functiontypescope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/mappedtypescope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/switchscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/tsenumscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/typescope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/withscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/scope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/classfieldinitializerscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scopemanager.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/blockscope.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/scope/index.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/referencer/visitorbase.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/referencer/patternvisitor.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/referencer/visitor.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/referencer/referencer.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/referencer/index.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/analyze.d.ts","../node_modules/@typescript-eslint/scope-manager/dist/index.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/scope.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/parser.d.ts","../node_modules/@typescript-eslint/utils/dist/json-schema.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/sourcecode.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/rule.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/linter.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/processor.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/config.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/eslint/eslintshared.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/eslint/flateslint.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/eslint/legacyeslint.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/eslint.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/ruletester.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-eslint/index.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/eslint-utils/astutilities.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/eslint-utils/patternmatcher.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/eslint-utils/predicates.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/eslint-utils/referencetracker.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/eslint-utils/scopeanalysis.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/eslint-utils/index.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/helpers.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/misc.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/predicates.d.ts","../node_modules/@typescript-eslint/utils/dist/ast-utils/index.d.ts","../node_modules/@typescript-eslint/utils/dist/eslint-utils/applydefault.d.ts","../node_modules/@typescript-eslint/utils/dist/eslint-utils/deepmerge.d.ts","../node_modules/@typescript-eslint/utils/dist/eslint-utils/getparserservices.d.ts","../node_modules/@typescript-eslint/utils/dist/eslint-utils/infertypesfromrule.d.ts","../node_modules/@typescript-eslint/utils/dist/eslint-utils/nullthrows.d.ts","../node_modules/@typescript-eslint/utils/dist/eslint-utils/rulecreator.d.ts","../node_modules/@typescript-eslint/utils/dist/eslint-utils/index.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-utils/isarray.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-utils/noinfer.d.ts","../node_modules/@typescript-eslint/utils/dist/ts-utils/index.d.ts","../node_modules/@typescript-eslint/utils/dist/index.d.ts","../node_modules/typescript-eslint/dist/compatibility-types.d.ts","../node_modules/typescript-eslint/dist/config-helper.d.ts","../node_modules/typescript-eslint/dist/globs.d.ts","../node_modules/typescript-eslint/dist/index.d.ts","../eslint.config.ts","../node_modules/@vitest/spy/optional-types.d.ts","../node_modules/@vitest/spy/dist/index.d.ts","../node_modules/tinyrainbow/dist/index.d.ts","../node_modules/@standard-schema/spec/dist/index.d.ts","../node_modules/@vitest/pretty-format/dist/index.d.ts","../node_modules/@vitest/utils/dist/types.d-bcelap-c.d.ts","../node_modules/@vitest/utils/dist/diff.d.ts","../node_modules/@vitest/utils/dist/display.d.ts","../node_modules/@types/deep-eql/index.d.ts","../node_modules/assertion-error/index.d.ts","../node_modules/@types/chai/index.d.ts","../node_modules/@vitest/expect/dist/index.d.ts","../node_modules/@vitest/utils/dist/types.d.ts","../node_modules/@vitest/utils/dist/helpers.d.ts","../node_modules/@vitest/utils/dist/timers.d.ts","../node_modules/@vitest/utils/dist/index.d.ts","../node_modules/@vitest/runner/dist/tasks.d-deyaimiu.d.ts","../node_modules/@vitest/runner/dist/index.d.ts","../node_modules/vitest/dist/chunks/traces.d.d2t_r8rx.d.ts","../node_modules/@vitest/snapshot/dist/environment.d-dojxxzv9.d.ts","../node_modules/@vitest/snapshot/dist/rawsnapshot.d-d_x3-62x.d.ts","../node_modules/@vitest/snapshot/dist/index.d.ts","../node_modules/vitest/dist/chunks/config.d.a1h_y6jt.d.ts","../node_modules/vitest/dist/chunks/environment.d.crsxczp1.d.ts","../node_modules/vitest/dist/chunks/rpc.d.b_8spu0w.d.ts","../node_modules/vitest/dist/chunks/worker.d.zphpo4yb.d.ts","../node_modules/vitest/dist/chunks/browser.d.bcoexmfg.d.ts","../node_modules/vitest/optional-types.d.ts","../node_modules/@vitest/runner/dist/utils.d.ts","../node_modules/tinybench/dist/index.d.ts","../node_modules/vitest/dist/chunks/benchmark.d.daahlpsq.d.ts","../node_modules/@vitest/mocker/dist/types.d-bji5eawu.d.ts","../node_modules/@vitest/mocker/dist/index.d-b41z0auw.d.ts","../node_modules/@vitest/mocker/dist/index.d.ts","../node_modules/@vitest/utils/dist/source-map.d.ts","../node_modules/vitest/dist/chunks/coverage.d.bztk59wp.d.ts","../node_modules/@vitest/utils/dist/serialize.d.ts","../node_modules/@vitest/utils/dist/error.d.ts","../node_modules/vitest/dist/browser.d.ts","../node_modules/vitest/browser/context.d.ts","../node_modules/@vitest/snapshot/dist/manager.d.ts","../node_modules/vitest/dist/chunks/reporters.d.dtokvv2s.d.ts","../node_modules/vitest/dist/chunks/plugin.d.dwfiij7i.d.ts","../node_modules/vitest/dist/config.d.ts","../node_modules/vitest/config.d.ts","../vitest.config.ts","../postcss.config.ts","../tailwind.config.ts","../src/types/common/label.v1.d.ts","../src/types/common/publication.v1.d.ts","../src/types/common/countrycode.v1.d.ts","../src/types/common/geography.v1.d.ts","../src/types/common/emissionsscope.v1.d.ts","../src/types/common/metric.v1.d.ts","../src/types/pathwaymetadata.v1.d.ts","../src/types/common/scopesector.v2.d.ts","../src/types/common/scopegeography.v2.d.ts","../src/types/pathwaymetadata.v2.d.ts","../src/types/index.ts","../node_modules/i18n-iso-countries/index.d.ts","../node_modules/i18n-iso-countries/langs/en.json","../src/utils/geographyutils.ts","../src/utils/absent.ts","../src/utils/filterregions.ts","../src/utils/facets.ts","../src/utils/keyfeaturescope.ts","../src/utils/sortutils.ts","../src/data/index.gen.ts","../src/utils/searchutils.ts","../src/schema/pathwaymetadata.v2.json","../src/data/pathwaymetadata.ts","../src/utils/numericrangelimits.ts","../node_modules/vitest/dist/chunks/global.d.dvssrdq5.d.ts","../node_modules/vitest/optional-runtime-types.d.ts","../node_modules/vitest/dist/chunks/suite.d.udjtyagw.d.ts","../node_modules/vitest/dist/chunks/evaluatedmodules.d.bxj5omdx.d.ts","../node_modules/vitest/dist/runners.d.ts","../node_modules/expect-type/dist/utils.d.ts","../node_modules/expect-type/dist/overloads.d.ts","../node_modules/expect-type/dist/branding.d.ts","../node_modules/expect-type/dist/messages.d.ts","../node_modules/expect-type/dist/index.d.ts","../node_modules/vitest/dist/index.d.ts","../src/utils/absent.test.ts","../src/utils/charttooltiplayout.ts","../src/utils/charttooltiplayout.test.ts","../src/utils/facets.test.ts","../src/utils/filterregions.test.ts","../src/utils/gettemperaturecolor.ts","../src/utils/keyfeaturescope.test.ts","../src/utils/normalizeabsent.ts","../src/utils/normalizeabsent.test.ts","../src/utils/searchutils.scopedfacets.test.ts","../src/utils/timeseriestaxonomy.ts","../src/utils/timeseriesavailability.ts","../src/utils/timeseriesavailability.test.ts","../src/utils/timeseriesindex.ts","../src/utils/timeseriesindex.test.ts","../src/utils/timeseriestaxonomy.test.ts","../src/utils/tooltiputils.ts","../src/utils/validatescopes.ts","../src/utils/validatescopes.test.ts"],"fileIdsList":[[61,126,134,139,142,144,145,146,158,349,350,352,353,477],[61,126,134,139,142,144,145,146,158,348],[61,126,134,139,142,144,145,146,158,344],[61,126,134,139,142,144,145,146,158,345,346],[61,126,134,139,142,144,145,146,158,345],[61,126,134,139,142,144,145,146,158],[61,126,134,139,142,144,145,146,158,192],[61,126,134,139,142,144,145,146,158,268],[61,126,134,139,142,144,145,146,158,242,243,244,245,246],[61,126,134,139,142,144,145,146,158,243],[61,126,134,139,142,144,145,146,158,242],[61,126,134,139,142,144,145,146,158,242,243],[61,126,134,139,142,144,145,146,158,487,488],[61,123,124,126,134,139,142,144,145,146,158],[61,125,126,134,139,142,144,145,146,158],[126,134,139,142,144,145,146,158],[61,126,134,139,142,144,145,146,158,167],[61,126,127,132,134,137,139,142,144,145,146,148,158,163,176],[61,126,127,128,134,137,139,142,144,145,146,158],[61,126,129,134,139,142,144,145,146,158,177],[61,126,130,131,134,139,142,144,145,146,149,158],[61,126,131,134,139,142,144,145,146,158,163,173],[61,126,132,134,137,139,142,144,145,146,148,158],[61,125,126,133,134,139,142,144,145,146,158],[61,126,134,135,139,142,144,145,146,158],[61,126,134,136,137,139,142,144,145,146,158],[61,125,126,134,137,139,142,144,145,146,158],[61,126,134,137,139,140,142,144,145,146,158,163,176],[61,126,134,137,139,140,142,144,145,146,158,163,165,167],[61,113,126,134,137,139,141,142,144,145,146,148,158,163,176],[61,126,134,137,139,141,142,144,145,146,148,158,163,173,176],[61,126,134,139,141,142,143,144,145,146,158,163,173,176],[60,61,62,63,64,65,66,67,68,69,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184],[61,126,134,137,139,142,144,145,146,158],[61,126,134,139,142,144,146,158],[61,126,134,139,142,144,145,146,147,158,176],[61,126,134,137,139,142,144,145,146,148,158,163],[61,126,134,139,142,144,145,146,149,158],[61,126,134,139,142,144,145,146,150,158],[61,126,134,137,139,142,144,145,146,153,158],[61,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183],[61,126,134,139,142,144,145,146,155,158],[61,126,134,139,142,144,145,146,156,158],[61,126,131,134,139,140,142,144,145,146,148,158,165,173],[61,126,134,137,139,142,144,145,146,158,159],[61,126,134,139,142,144,145,146,158,160,177,180],[61,126,134,137,139,142,144,145,146,158,163,165,166,167],[61,126,134,139,142,144,145,146,158,164,167],[61,126,134,137,139,142,144,145,146,158,163,165],[61,126,134,137,139,142,144,145,146,158,163,166,167],[61,126,134,139,142,144,145,146,158,167,177],[61,126,134,139,142,144,145,146,158,168],[61,123,126,134,139,142,144,145,146,158,163,170,176],[61,126,134,139,142,144,145,146,158,163,169],[61,126,134,137,139,142,144,145,146,158,171,172],[61,126,134,139,142,144,145,146,158,171,172],[61,126,131,134,139,142,144,145,146,148,158,163,173],[61,126,134,139,142,144,145,146,158,174],[61,126,134,139,142,144,145,146,148,158,175],[61,126,134,139,141,142,144,145,146,156,158,176],[61,126,134,139,142,144,145,146,158,177,178],[61,126,131,134,139,142,144,145,146,158,178],[61,126,134,139,142,144,145,146,158,163,179],[61,126,134,139,142,144,145,146,147,158,180],[61,126,134,139,142,144,145,146,158,181],[61,126,129,134,139,142,144,145,146,158],[61,126,131,134,139,142,144,145,146,158],[61,126,134,139,142,144,145,146,158,177],[61,113,126,134,139,142,144,145,146,158],[61,126,134,139,142,144,145,146,158,176],[61,126,134,139,142,144,145,146,158,182],[61,126,134,139,142,144,145,146,153,158],[61,126,134,139,142,144,145,146,158,172],[61,113,126,134,137,139,140,142,144,145,146,153,158,163,167,176,179,180,182],[61,126,134,139,142,144,145,146,158,163,183],[61,126,134,139,142,144,145,146,158,165,184],[61,126,134,139,142,144,145,146,158,325,326],[61,126,134,139,142,144,145,146,158,327],[61,126,134,139,142,144,145,146,158,359,362],[61,126,134,139,142,144,145,146,158,363],[61,126,134,139,142,144,145,146,158,359,429,436],[61,126,134,139,142,144,145,146,158,359,388,389],[61,126,134,139,142,144,145,146,158,390,391,392,393,394,395,396,397,398,399,400],[61,126,134,139,142,144,145,146,158,359,388],[61,126,134,139,142,144,145,146,158,388,390,391,392,393,394,395,396,397,398,399,400,401],[61,126,134,139,142,144,145,146,158,402,403,408,429,431,433,434,437],[61,126,134,139,142,144,145,146,158,435],[61,126,134,139,142,144,145,146,158,359,432],[61,126,134,139,142,144,145,146,158,359,408,431],[61,126,134,139,142,144,145,146,158,359,403,429,431,434],[61,126,134,139,142,144,145,146,158,359,432,433],[61,126,134,139,142,144,145,146,158,359,379],[61,126,134,139,142,144,145,146,158,359,409,414,427,429],[61,126,134,139,142,144,145,146,158,359,403,408,409,414,427,429],[61,126,134,139,142,144,145,146,158,359,408,409,414,427,429],[61,126,134,139,142,144,145,146,158,409,410,411,412,413,415,416,417,418,419,420,421,422,423,424,425,426,427,428,430],[61,126,134,139,142,144,145,146,158,410,411,412,413,415,416,417,418,419,420,421,422,423,424,425,426,428,430],[61,126,134,139,142,144,145,146,158,359,402,403,408,409,410,411,412,413,427,429],[61,126,134,139,142,144,145,146,158,359,408,417,428,431],[61,126,134,139,142,144,145,146,158,359,404],[61,126,134,139,142,144,145,146,158,405,406,431],[61,126,134,139,142,144,145,146,158,405,406,407],[61,126,134,139,142,144,145,146,158,404],[61,126,134,139,142,144,145,146,158,359,402,403,431],[61,126,134,139,142,144,145,146,158,354],[61,126,134,139,142,144,145,146,158,355,356,357,358],[61,126,134,139,142,144,145,146,158,354,356],[61,126,134,139,142,144,145,146,158,355,358],[61,126,134,139,142,144,145,146,158,354,369],[61,126,134,139,142,144,145,146,158,354,369,370],[61,126,134,139,142,144,145,146,158,360,361,367,370,371,372,373,374,375,376,380,381,382,383],[61,126,134,139,142,144,145,146,158,354,367],[61,126,134,139,142,144,145,146,158,354,359,367],[61,126,134,139,142,144,145,146,158,354,367,374],[61,126,134,139,142,144,145,146,158,359],[61,126,134,139,142,144,145,146,158,354,364,365,367,368,370],[61,126,134,139,142,144,145,146,158,367,379],[61,126,134,139,142,144,145,146,158,354,359,365],[61,126,134,139,142,144,145,146,158,359,365,366],[61,126,134,139,142,144,145,146,158,385,452],[61,126,134,139,142,144,145,146,158,453,454,455,456,457],[61,126,134,139,142,144,145,146,158,385],[61,126,134,139,142,144,145,146,158,458,459,460,461],[61,126,134,139,142,144,145,146,158,463,464,465,466,467,468],[61,126,134,139,142,144,145,146,158,452],[61,126,134,139,142,144,145,146,158,443],[61,126,134,139,142,144,145,146,158,385,441,452,462,469,472],[61,126,134,139,142,144,145,146,158,387,440,443,445],[61,126,134,139,142,144,145,146,158,448,449],[61,126,134,139,142,144,145,146,158,443,444],[61,126,134,139,142,144,145,146,158,446,447],[61,126,134,139,142,144,145,146,158,444,446,447],[61,126,134,139,142,144,145,146,158,386,387,439,440,442,443,444,445,446,450,451],[61,126,134,139,142,144,145,146,158,440,442,443,445,446],[61,126,134,139,142,144,145,146,158,385,387,439],[61,126,134,139,142,144,145,146,158,444],[61,126,134,139,142,144,145,146,158,385,386,439,441,442,444,446],[61,126,134,139,142,144,145,146,158,385,387,443,444,446],[61,126,134,139,142,144,145,146,158,438],[61,126,134,139,142,144,145,146,158,385,439,440],[61,126,134,139,142,144,145,146,158,359,384],[61,126,134,139,142,144,145,146,158,470,471],[61,126,134,139,142,144,145,146,158,377,378],[61,126,134,139,142,144,145,146,158,234,235,522],[61,126,134,139,142,144,145,146,158,480,481,482,485,486,489],[61,126,134,139,142,144,145,146,158,510],[61,126,134,139,142,144,145,146,158,510,511],[61,126,134,139,142,144,145,146,158,485,494,495],[61,126,134,139,142,144,145,146,158,485,494],[61,126,134,139,142,144,145,146,158,494],[61,126,134,139,142,144,145,146,158,483,494,498,499],[61,126,134,139,142,144,145,146,158,483,494,498],[61,126,134,139,142,144,145,146,158,479],[61,126,134,139,142,144,145,146,158,483,484],[61,126,134,139,142,144,145,146,158,483],[61,126,134,139,142,144,145,146,158,483,484,491,515],[61,126,134,139,142,144,145,146,158,491],[61,126,134,139,142,144,145,146,158,483,486,491,492,493],[61,126,134,139,142,144,145,146,158,315],[61,126,134,139,142,144,145,146,158,315,316,317],[61,126,134,139,142,144,145,146,158,274,275,279,306,307,309,310,311,313,314],[61,126,134,139,142,144,145,146,158,272,273],[61,126,134,139,142,144,145,146,158,272],[61,126,134,139,142,144,145,146,158,274,314],[61,126,134,139,142,144,145,146,158,274,275,311,312,314],[61,126,134,139,142,144,145,146,158,314],[61,126,134,139,142,144,145,146,158,271,314,315],[61,126,134,139,142,144,145,146,158,274,275,313,314],[61,126,134,139,142,144,145,146,158,274,275,277,278,313,314],[61,126,134,139,142,144,145,146,158,274,275,276,313,314],[61,126,134,139,142,144,145,146,158,274,275,279,306,307,308,309,310,313,314],[61,126,134,139,142,144,145,146,158,271,274,275,279,311,313],[61,126,134,139,142,144,145,146,158,279,314],[61,126,134,139,142,144,145,146,158,281,282,283,284,285,286,287,288,289,290,314],[61,126,134,139,142,144,145,146,158,304,314],[61,126,134,139,142,144,145,146,158,280,291,299,300,301,302,303,305],[61,126,134,139,142,144,145,146,158,284,314],[61,126,134,139,142,144,145,146,158,292,293,294,295,296,297,298,314],[61,126,134,137,139,142,144,145,146,158,185,321],[61,126,134,139,142,144,145,146,158,343,348],[61,126,134,139,142,144,145,146,158,351],[61,126,134,139,142,144,145,146,158,343,345,347],[61,126,134,139,142,144,145,146,158,556,557],[61,126,134,139,142,144,145,146,158,556,557,558,559],[61,126,134,139,142,144,145,146,158,556,558],[61,126,134,139,142,144,145,146,158,556],[61,126,134,139,142,144,145,146,158,328],[61,126,134,139,142,144,145,146,158,222],[61,126,134,139,142,144,145,146,158,220,222],[61,126,134,139,142,144,145,146,158,211,219,220,221,223,225],[61,126,134,139,142,144,145,146,158,209],[61,126,134,139,142,144,145,146,158,212,217,222,225],[61,126,134,139,142,144,145,146,158,208,225],[61,126,134,139,142,144,145,146,158,212,213,216,217,218,225],[61,126,134,139,142,144,145,146,158,212,213,214,216,217,225],[61,126,134,139,142,144,145,146,158,209,210,211,212,213,217,218,219,221,222,223,225],[61,126,134,139,142,144,145,146,158,225],[61,126,134,139,142,144,145,146,158,207,209,210,211,212,213,214,216,217,218,219,220,221,222,223,224],[61,126,134,139,142,144,145,146,158,207,225],[61,126,134,139,142,144,145,146,158,212,214,215,217,218,225],[61,126,134,139,142,144,145,146,158,216,225],[61,126,134,139,142,144,145,146,158,217,218,222,225],[61,126,134,139,142,144,145,146,158,210,220],[61,126,134,139,142,144,145,146,158,194],[61,126,134,139,142,144,145,146,158,189,191,194],[61,126,134,139,142,144,145,146,158,190,191],[61,126,134,139,142,144,145,146,158,191,194,198],[61,126,134,139,142,144,145,146,158,190],[61,126,134,139,142,144,145,146,158,191,194],[61,126,134,139,142,144,145,146,158,189,190,191,193],[61,126,134,139,142,144,145,146,158,189,191],[61,126,134,139,142,144,145,146,158,190,191,200],[61,126,134,139,142,144,145,146,158,240,248],[61,126,134,139,142,144,145,146,158,248],[61,126,134,139,142,144,145,146,158,240],[61,126,134,139,142,144,145,146,158,238,248,270],[61,126,134,139,142,144,145,146,158,270],[61,126,134,139,142,144,145,146,158,248,270],[61,126,127,134,139,142,144,145,146,158,239,241,247],[61,126,134,139,142,144,145,146,158,238,248],[61,126,134,139,142,144,145,146,158,240,262,263,264,265],[61,126,134,139,142,144,145,146,158,250,261,266,267,269],[61,126,134,139,142,144,145,146,158,237,249],[61,126,134,139,142,144,145,146,158,250,261,266],[61,126,134,139,142,144,145,146,158,237,248,249,251,252,253,254,255,256,257,258,259,260],[61,126,134,139,142,144,145,146,158,473],[61,126,134,139,142,144,145,146,158,452,473,474,475,476],[61,76,79,82,83,126,134,139,142,144,145,146,158,176],[61,79,126,134,139,142,144,145,146,158,163,176],[61,79,83,126,134,139,142,144,145,146,158,176],[61,126,134,139,142,144,145,146,158,163],[61,73,126,134,139,142,144,145,146,158],[61,77,126,134,139,142,144,145,146,158],[61,75,76,79,126,134,139,142,144,145,146,158,176],[61,126,134,139,142,144,145,146,148,158,173],[61,126,134,139,142,144,145,146,158,185],[61,73,126,134,139,142,144,145,146,158,185],[61,75,79,126,134,139,142,144,145,146,148,158,176],[61,70,71,72,74,78,126,134,137,139,142,144,145,146,158,163,176],[61,79,126,134,139,142,144,145,146,158],[61,79,88,97,126,134,139,142,144,145,146,158],[61,71,77,126,134,139,142,144,145,146,158],[61,79,107,108,126,134,139,142,144,145,146,158],[61,71,74,79,126,134,139,142,144,145,146,158,167,176,185],[61,75,79,126,134,139,142,144,145,146,158,176],[61,70,126,134,139,142,144,145,146,158],[61,73,74,75,77,78,79,80,81,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,108,109,110,111,112,126,134,139,142,144,145,146,158],[61,79,100,103,126,134,139,142,144,145,146,158],[61,79,88,90,91,126,134,139,142,144,145,146,158],[61,77,79,90,92,126,134,139,142,144,145,146,158],[61,78,126,134,139,142,144,145,146,158],[61,71,73,79,126,134,139,142,144,145,146,158],[61,79,83,90,92,126,134,139,142,144,145,146,158],[61,83,126,134,139,142,144,145,146,158],[61,77,79,82,126,134,139,142,144,145,146,158,176],[61,71,75,79,88,126,134,139,142,144,145,146,158],[61,79,100,126,134,139,142,144,145,146,158],[61,92,126,134,139,142,144,145,146,158],[61,71,75,79,83,126,134,139,142,144,145,146,158],[61,73,79,107,126,134,139,142,144,145,146,158,167,182,185],[61,126,134,139,142,144,145,146,158,234,322,522],[61,126,134,139,142,144,145,146,158,186],[61,126,134,137,139,141,142,143,144,145,146,148,158,163,173,176,183,185,186,187,188,195,196,197,199,201,203,204,205,206,226,230,231,232,233,234],[61,126,134,139,142,144,145,146,158,186,187,188,202],[61,126,134,139,142,144,145,146,158,227,228],[61,126,134,139,142,144,145,146,158,188],[61,126,134,139,142,144,145,146,158,229],[61,126,134,139,142,144,145,146,158,195,205,234],[61,126,134,139,142,144,145,146,158,195,234],[61,126,134,139,142,144,145,146,158,517],[61,126,134,139,142,144,145,146,158,490,522,551],[61,126,134,139,142,144,145,146,158,480,483,485,486,492,493,494,496,497,500,501,513,514,516,551],[61,126,134,139,142,144,145,146,158,496,507,508,551],[61,126,134,139,142,144,145,146,158,496,497,504,551],[61,126,134,139,142,144,145,146,158,483,485,496,497,500,551],[61,126,134,139,142,144,145,146,158,203],[61,126,134,139,142,144,145,146,158,483,490,496,497,500,509,551],[61,126,134,139,142,144,145,146,158,234,520,522],[61,126,129,134,139,142,144,145,146,158,163,234,483,485,490,494,496,497,500,501,504,505,506,509,512,513,514,518,519,522,551],[61,126,134,139,142,144,145,146,158,203,496,497,500,551],[61,126,134,139,142,144,145,146,158,496,507,508,509,551],[61,126,134,139,142,144,145,146,158,203,496,501,502,503,551],[61,126,129,134,139,142,144,145,146,158,163,203,234,483,485,490,494,496,497,500,501,502,503,504,505,506,507,508,509,512,513,514,518,519,520,521,522,551],[61,126,134,139,142,144,145,146,158,203,480,483,485,490,494,496,497,500,501,502,503,504,505,507,508,509,512,551,552,553,554,555,560],[61,126,134,139,142,144,145,146,158,483,485,496,497,500,501,507,508,509,551,553],[61,126,134,139,142,144,145,146,158,319,320,329,341,537,548],[61,126,134,139,142,144,145,146,158,315,330,331,332,333,334,335,336,337,338,339,340],[61,126,134,139,142,144,145,146,158,529],[61,126,134,139,142,144,145,146,158,527],[61,126,134,139,142,144,145,146,158,528,530,533,536,547],[61,126,134,139,142,144,145,146,158,527,528,530,531,532],[61,126,134,139,142,144,145,146,158,527,528,530,531,532,534,535],[61,126,134,139,142,144,145,146,158,541,561],[61,126,134,139,142,144,145,146,158,561,563],[61,126,134,139,142,144,145,146,158,541,543,561],[61,126,134,139,142,144,145,146,158,541],[61,126,134,139,142,144,145,146,158,341,541,542,561],[61,126,134,139,142,144,145,146,158,341,537,540,541],[61,126,134,139,142,144,145,146,158,537,538,539],[61,126,134,139,142,144,145,146,158,537,541,544,561],[61,126,134,139,142,144,145,146,158,537,540,541,542],[61,126,134,139,142,144,145,146,158,315,319],[61,126,134,139,142,144,145,146,158,541,561,569],[61,126,134,139,142,144,145,146,158,547,549],[61,126,134,139,142,144,145,146,158,537,541,547,561],[61,126,134,139,142,144,145,146,158,537,540,541,542,543,544,545,546],[61,126,134,139,142,144,145,146,158,540],[61,126,134,139,142,144,145,146,158,561,573],[61,126,134,139,142,144,145,146,158,540,572],[61,126,134,139,142,144,145,146,158,561,575],[61,126,134,139,142,144,145,146,158,546],[61,126,134,139,142,144,145,146,158,331,332,561,572],[61,126,134,139,142,144,145,146,158,537],[61,126,134,139,142,144,145,146,158,315,318],[61,126,134,139,142,144,145,146,158,331,537,561,579],[61,126,134,139,142,144,145,146,158,536,572],[61,126,134,139,140,141,142,144,145,146,149,150,158,234,236,270,319,320,323,324,329,341,522],[61,126,134,139,142,144,145,146,158,236,523]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"f53a7652392cf26ebbe4e29fd0672aa87c93bd6d0241289c13fab87b9ac35c8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"b00a630557d1622ad312633bdbfbdb9c6b7220d948dca9f899db30679f160074","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"88809d6c1b9c78d04a133646a6feb926def05a8774c308c7c93bc32ee163d271","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"3ac40516c33b87f751f7507346933081a26cdb8a3e11a6b3aa07d23f803c85db","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"829bc57ee8f287b490ab5bbc5a962fca57e432c1e38ec680ecd3ecaf12800613","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"3a3ff14da53d5013f3e6d8c4ad55225e3649c08786c4421ce639c00d8d589b7d","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"38004e6801340cb890afb8cb5a9fc8972297e7f88ab94026e4b0b3c61fb32f8a","impliedFormat":1},{"version":"d243db6b25788f439e7e2f03c05688e92f46764351673bb0e7b2f3631232e186","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"149f9a9e7f04e67afa0e2a49fc0ff421035c01d6b793cfcae7d2e9f6819431e2","impliedFormat":1},{"version":"e8a9dfa4c75ef6d25df8b40eaa9c31e0a69452aaf2ced4a3f4215dbdbaa876f4","impliedFormat":1},{"version":"a70af845a2eb9dd6e2723e319e14ea7fb28b129ec1361c21509b49305448c323","impliedFormat":1},{"version":"b53dc572d4f187904207ae1166652de47aab8eeb00c254d009cb226863076b56","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"f501234c5aeeeb5d7659412335227466aaacf30b952372d60afeb21c02c96348","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"9ac977503f15bf13ca7c82ad9a32a782f42d43e474824e8b3bffe228fd5f2639","impliedFormat":1},{"version":"8b91ff5bb912be3ea213cbcf0075aace1f5d4ff249a0d227ed673868cb7bfabc","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"7cb4f431a4591b0e23002bed805dc871a874dfed6b9a3994dce68a6df73e6739","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"e99963ae1e3a48ca7a7958c02f3e88bb963eb7978c28b68ae6b8c9f03309d83c","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"594ae90cacd813fa392ff80d2e0a8eff4e41f4a136329a940e321399dd8895b4","impliedFormat":1},{"version":"d1f333ade8ff35a409b4984e1f11956fe11c61855b0c7e9b59f0313e48b40c4a","impliedFormat":1},{"version":"0224aa4b3464895d69c413a640a19ac2166778a74eb5eb3b36b021c72d42b274","impliedFormat":1},{"version":"2e6fca0024dd8a076a886d9ec031a936ea59ad878a25b944820495d92f8381dd","affectsGlobalScope":true,"impliedFormat":1},{"version":"47d917e731d06acf182baa38ac2fbd720c3555fa9160e6a40d47994515f7f0ea","impliedFormat":1},{"version":"2fbf504c4791f9d32cd766cfe6b605bcda63289b925401953a7900db9af85348","impliedFormat":1},{"version":"ed0fb633cae35948d9e144004299a4bdf1ab912667c787b7fbffcd6d8c7b92a2","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"aa7b2f4e5f2fced2d34405e729ee7e09903d3330fb896a8b3d44c8fbd16be04d","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"98fc7231d6c846a8ed19bdef026ddffe5e2182cc818be74899287421ad370a87","impliedFormat":1},{"version":"e494b286c8e27c52f8c7f945aa80ff223f2a3cd4f7a2619fa8991f1459e26f07","impliedFormat":1},{"version":"f54f5f9e10bbceba7515dfad0ffc89dffdb8d749d0604594c57a725cf6d79c20","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"8c7e618c2a91ea7f6b5cca272a295864e92c16413be8fc56a943e8c7d5320011","affectsGlobalScope":true,"impliedFormat":1},{"version":"79a85c64bf083c5f4dd7074e804d1000c0c47301b84d48417a224decd681be30","impliedFormat":1},{"version":"6d81823c5704398a68b98e3c7a459334fc0403ad5e6953f4317b5b4919e289e4","impliedFormat":1},{"version":"4f1fd541f720030367b5d7ebfea7ae5f9edc8481422993b9a363f459c5412437","impliedFormat":1},{"version":"baa9cafc61ef8b42b6e8f7a1aa1f75841ec9c9d2f59229277ed282ccb99b72c9","impliedFormat":1},{"version":"af960bc194044cd1f0c8a9857461f0e5fd88a5e33326f38a738d6ef0e1b56fbf","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"57a0d1b3d59063d4af2d3f8aac27cfe3c20a0f5d1faf0f8598ccf0b779637939","impliedFormat":1},{"version":"5ff4433a2deae4f85ab1377e90a7554ce6b47ae51c69a84ca30a6e22fae85834","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"97234c5303866576f913d0ccae7d58d6322d9e803c7bf1228a3fda46ab8087b2","affectsGlobalScope":true,"impliedFormat":1},{"version":"93ecf87143cac7b9d05cffc1d6bdc075b7e4fdd48ff05f1fad85043f6ae678d3","impliedFormat":1},{"version":"73eeb740dacc45adc607a4f59f90fa9dbe2717d93d82f115f45f34e18f8d644a","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"b17f272f903d155b9ec21791b5889b638977bc1dee181efb2bc317ce006d8c9b","impliedFormat":1},{"version":"1c16d778cc142455f35a3f1a411227b9901c671756338af12a9175958500ce66","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"1940d3dcf5bc8fdaf65d4a6fe08b9c9d6e6481186baec210d6e5ed57ced8c26b","impliedFormat":1},{"version":"441b9bb09013654aa3d050e68b06464d8959b473e85868249d9d18f692acd35b","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"55246f15e33ff1e2e9e679b25fa9790a48db55dc63d567fe25fac8b6a0efe911","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"9f979aaeb0e4ab8456942064596786aa2720c1984fea198ae0caa526f77547b1","impliedFormat":1},{"version":"5b26c4dd672d88336bb929787c9bfb55119e80ba89ec6dc923da1c063892843e","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"79e9c2d48d413b4d1f1c574d604e663231770e8883f53ba5773b32618aefd799","impliedFormat":1},{"version":"f12624a4a8d042b68914eac1b0a16571fc1c523173fcdf2517c65d191bd5a86c","impliedFormat":1},{"version":"b4769767f13a1692a66186e01c3aa186ff808d5ff72ed36eda8c37738fb2ac92","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"9b82f9d3cd32399278813e2de0eed4b059d362fbac5222c815e2ab01879893d7","impliedFormat":1},{"version":"1ec3f3a5f04cc42df33274fbe5c0937d9a9e06f249a7a26288d7d54f0763ffd4","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"e47f532d6e1617833f13a5b0710c0089d402c89c2f2b54f324e5a20e418d287a","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"8e2577e7262051fd3c5bd6ca2b2056d358ff8853565720f92455860824c25188","impliedFormat":1},{"version":"7ec8d7d483f7394f9da611b10d3a0e402f76ff5c991261e6ce43a15f81ca4258","impliedFormat":1},{"version":"bb9dbb4b2ad81e3e71ec5ba4314973718555b9d04ba2a17dfbf875efecb8e2c0","impliedFormat":1},{"version":"9021dc5be8f3a87af5cede29221188a6497e2092b6889e0be212ef6b6b7153e9","impliedFormat":1},{"version":"045a210189ec63c5488410b33f9fca53d77d051599d0d6506d8048551399c5f3","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"632c45ccb4cdc2c3a80c7a36a63dd7763016e59cac5e07bfb3e37e3548957028","impliedFormat":1},{"version":"c9a2daf6cd1eb854cd5b9e424247c5e306692055738c2effd35f7871d942b76e","impliedFormat":1},{"version":"afa1c49f8e559e413d57343339db857d2a8159435cf9cf7d4deb41718fff1b88","impliedFormat":1},{"version":"e50a7130339a951e6da9e968ed5521b0997a5b0479e148914087213839b5474c","impliedFormat":1},{"version":"64ccfe7dcbcad9b8a661a042ecb2309e41b9ab957f7b8cd070b05583a7821a13","impliedFormat":99},{"version":"c911b3bd083dade8fdb7775261aa25feae695d562a3b11209650b47be90043fc","impliedFormat":99},{"version":"98580d6ed68c9cee7f956685abbb6502d70bd92f48175ac807ae6c5e254fadc6","impliedFormat":99},{"version":"fc5ad2ec8b130a59612c5cfcc2cdca9bcdd80851adb8acdb004aaeff00cc978d","impliedFormat":99},{"version":"5c62f97efe6b4b10e8597cb6252fcf772eeea3d3e9b93e97d1ce3e24980042da","impliedFormat":99},{"version":"aacf8a6955a6e97c902e3ad9644afc21544795c0cd35327e063814a564f4d4b4","impliedFormat":99},{"version":"638a22f8e16b31c37297483fa38cfc760ed309b16d4c606a7b4532f4394c9c0c","impliedFormat":99},{"version":"9394183f4c37b8591156f32e41223c9e0dd211eefe7499155d4cdf15268eaea8","impliedFormat":99},{"version":"76bc3bb057ee2ed552c023a5cd259ce5a69de0c6cd2235c027c86411c625c95f","impliedFormat":99},{"version":"f4265b8ce9b8c5291370f8580562fadc65a1d796822b3d35a20642c471ca2f71","impliedFormat":99},{"version":"c0042ca8c8cbda9e978c4558933c47c812ac018e51d64ae8ff51e641672bed46","impliedFormat":99},{"version":"7d3e062a778b8f5ea4f0cac7e925e31f88e6739812ebc5f827474324a4048f14","impliedFormat":99},{"version":"cd9ae23a4728234472c4a90c8c12f9bd96f8909c4a8d7157b0210452bb3cd420","impliedFormat":99},{"version":"1210e9b360bc449574dd4b305b62e00f21a5e4ef27b43f3037eccf482e76eca7","impliedFormat":99},{"version":"08cad5153da753ed9c463b0e42a86bd8bc2b443368ce6a02a2a70e46adf7a4e0","impliedFormat":99},{"version":"675f5352f712f4335d9981907f1bddac7a5be2fd3a9f6a44e49f80133d369808","impliedFormat":99},{"version":"4e003c868b0d8f8ad200b96cbc653e18e513fa23e1c19c4fe3cc25d4394efc47","impliedFormat":99},{"version":"d4ec151557b64ff001e06b5878e66eb98640961647b1581fe2a0a1af05f408fb","impliedFormat":99},{"version":"e0864480ea083087d705f9405bd6bf59b795e8474c3447f0d6413b2bce535a09","impliedFormat":99},{"version":"e67cbea16f1994af89efd700542dbf3828a46a52b29e4d67e801bd7869dc103c","impliedFormat":99},{"version":"f582b0fcbf1eea9b318ab92fb89ea9ab2ebb84f9b60af89328a91155e1afce72","impliedFormat":99},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"3cc8a3d123b6b232d48d34b51b785f9da8d193f5b5817fa521fcd2f3b9315c55","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"354a7f8e1287d9d6b7561bc97fdd8cbc2f7c1dd79e4cb37b942e8a5cfaff1085","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"f3fe8197217cb6fd6e07c0eaa0966e4ece77cd779cbfc55767a93704cc692e04","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"617112682fcb55805f0a0e16450397edc016881962eee4298f4f4e3ef795aee8","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":99},{"version":"960bd764c62ac43edc24eaa2af958a4b4f1fa5d27df5237e176d0143b36a39c6","affectsGlobalScope":true,"impliedFormat":99},{"version":"209fec874a1a536ac3e7c1d2b53c4701e9d16f456de0ddc5abd0750905a0e0df","impliedFormat":99},{"version":"e50d04d261120f8f9d07e85698735d5b88f3a66bc395c08ce26c3d817d141a73","affectsGlobalScope":true,"impliedFormat":99},{"version":"343badbae019a9bc81f44b0942ab08f0702521429be21dba24a70238dba35645","impliedFormat":99},{"version":"2faebfa830ae4cfbfb58e48b0ec20a2a63882d776f0ca36ec7155d45cf1b7f2d","impliedFormat":99},{"version":"9b98d6982d3a305952855f00b660b60d0d625f0965281f1692d5df8a401fca00","impliedFormat":99},{"version":"16b81141d0c59af6f07e5fc24824c54dd6003da0ab0a2d2cedc95f8eb03ea8d3","impliedFormat":1},{"version":"6578758b0b94087beffd0ce554701365cd1e6a7428f14464ac8b88095fca4e50","impliedFormat":1},{"version":"b6c4796630a47f8b0f420519cd241e8e7701247b48ed4b205e8d057cbf7107d7","impliedFormat":1},{"version":"6256cf36c8ae7e82bff606595af8fe08a06f8478140fcf304ee2f10c7716ddc8","impliedFormat":1},{"version":"b2dbe6b053e04ec135c7ce722e0a4e9744281ea40429af96e2662cc926465519","impliedFormat":1},{"version":"bd2c8f750fc6fcb45d988603052e89ca7759ce482c49793786e341e262d4a4d6","impliedFormat":1},{"version":"775531f55189af09e075893722a81807b8a3b908e2a2bbb24d2cea4a76de9d38","impliedFormat":1},{"version":"eea465d51d6e997543be5d873145819d1279deda351015d2e836df6b06c6efe9","impliedFormat":1},{"version":"799b38ea6c500092c26b26a456e513cd347c38bf9d26b15c86cbba81bede1022","impliedFormat":1},{"version":"ddb546cbed105fce585d28603d1dc2a8351f8b81b16f69b67d0d29a7a4793042","impliedFormat":1},{"version":"fe0eaceca01f67de63e96e0082ea9d936ac94dffad131ffad65c4dcf3953d94e","impliedFormat":1},{"version":"b62b9d7193265de7526c4d5d777902fdb3150ec6773ab7dd642864d92b8c12a7","impliedFormat":1},{"version":"5619706bbd7a964d7c82cd4a307457ed0327ecc86772ceb7ea0870566c6578b2","impliedFormat":1},{"version":"b48c4e15766170c5003a6273b1d8f17f854ec565ccaaebd9f700fef159b84078","impliedFormat":1},{"version":"7c774169686976056434799723bd7a48348df9d2204b928a0b77920505585214","impliedFormat":1},{"version":"3e697e2186544103572756d80b61fcce3842ab07abdc5a1b7b8d4b9a4136005a","impliedFormat":1},{"version":"8758b438b12ea50fb8b678d29ab0ef42d77abfb801cec481596ce6002b537a6f","impliedFormat":1},{"version":"688a28e7953ef4465f68da2718dc6438aaa16325133a8cb903bf850c63cb4a7e","impliedFormat":1},{"version":"f2c96e813200ca900d2bbf0d8e3db7c2a70180b80f559b676aab8315c9472943","impliedFormat":1},{"version":"f73cf81342d2a25b65179c262ca7c38df023969129094607d0eb52510a56f10f","impliedFormat":1},{"version":"f433d28f86313073f13b16c0a18ccdd21759390f52c8d7bf9d916645b12d16ed","impliedFormat":1},{"version":"e7d7e67bd66b30f2216e4678b97bb09629a2b31766a79119acaa30e3005ef5fb","impliedFormat":1},{"version":"e05a20aa85c7324c65643542c2d7314774c2adf510f9dcbad5d3afac74ca3dac","impliedFormat":1},{"version":"e137f087bda0256410b28743ef9a1bf57a4cafd43ffa6b62d5c17a8f5a08b3b5","impliedFormat":1},{"version":"dd3e50008131e83844c49f80f7dc8e5a8fd8d1fbe0bc4da26121dda14488e507","impliedFormat":1},{"version":"af504042a6db047c40cc0aeb14550bbc954f194f2b8c5ad8944f2da502f45bf5","impliedFormat":1},{"version":"5b25b6ab5ad6c17f90b592162b2e9978ad8d81edf24cd3957306eb6e5edb89a9","impliedFormat":1},{"version":"24693bd77ac3be0b16e564d0ab498a397feb758ce7f4ed9f13478d566e3aafde","impliedFormat":1},{"version":"208dad548b895c7d02465de6ba79064b7c67bc4d94e5227b09f21d58790e634c","impliedFormat":1},{"version":"048c0ced65fa41fbf4bcc3d5e8e5b6f6c7f27335ceb54d401be654e821adbc08","impliedFormat":1},{"version":"e1126668c194faa56a728eb5bd2dd88dfc19460ced65c38888977a22369e4624","impliedFormat":1},{"version":"1df8dcaa6969c836cfe30ae6c0b91d2d94c8706be13ac55d203a639741908a76","impliedFormat":1},{"version":"e84e587a2392736f944496a5805c787473fd3edc5a5d8046c0063b2334c05fcc","impliedFormat":1},{"version":"fe54919026a7e4762fc03f58baa8a2bf856af8de0eaae0a0d27265fa122565e5","impliedFormat":1},{"version":"84bcc7c6b06f4d643a55dc63b56be0c81d990f8d549b66ea615c553268774dc3","impliedFormat":1},{"version":"2d225e7bda2871c066a7079c88174340950fb604f624f2586d3ea27bb9e5f4ff","impliedFormat":1},{"version":"6a785f84e63234035e511817dd48ada756d984dd8f9344e56eb8b2bdcd8fd001","impliedFormat":1},{"version":"c1422d016f7df2ccd3594c06f2923199acd09898f2c42f50ea8159f1f856f618","impliedFormat":1},{"version":"2973b1b7857ca144251375b97f98474e9847a890331e27132d5a8b3aea9350a8","impliedFormat":1},{"version":"0eb6152d37c84d6119295493dfcc20c331c6fda1304a513d159cdaa599dcb78b","impliedFormat":1},{"version":"237df26f8c326ca00cd9d2deb40214a079749062156386b6d75bdcecc6988a6b","impliedFormat":1},{"version":"cd44995ee13d5d23df17a10213fed7b483fabfd5ea08f267ab52c07ce0b6b4da","impliedFormat":1},{"version":"58ce1486f851942bd2d3056b399079bc9cb978ec933fe9833ea417e33eab676e","impliedFormat":1},{"version":"7557d4d7f19f94341f4413575a3453ba7f6039c9591015bcf4282a8e75414043","impliedFormat":1},{"version":"a3b2cc16f3ce2d882eca44e1066f57a24751545f2a5e4a153d4de31b4cac9bb5","impliedFormat":1},{"version":"ac2b3b377d3068bfb6e1cb8889c99098f2c875955e2325315991882a74d92cc8","impliedFormat":1},{"version":"8deb39d89095469957f73bd194d11f01d9894b8c1f1e27fbf3f6e8122576b336","impliedFormat":1},{"version":"a38a9c41f433b608a0d37e645a31eecf7233ef3d3fffeb626988d3219f80e32f","impliedFormat":1},{"version":"8e1428dcba6a984489863935049893631170a37f9584c0479f06e1a5b1f04332","impliedFormat":1},{"version":"1fce9ecb87a2d3898941c60df617e52e50fb0c03c9b7b2ba8381972448327285","impliedFormat":1},{"version":"5ef0597b8238443908b2c4bf69149ed3894ac0ddd0515ac583d38c7595b151f1","impliedFormat":1},{"version":"ac52b775a80badff5f4ac329c5725a26bd5aaadd57afa7ad9e98b4844767312a","impliedFormat":1},{"version":"6ae5b4a63010c82bf2522b4ecfc29ffe6a8b0c5eea6b2b35120077e9ac54d7a1","impliedFormat":1},{"version":"dd7109c49f416f218915921d44f0f28975df78e04e437c62e1e1eb3be5e18a35","impliedFormat":1},{"version":"eee181112e420b345fc78422a6cc32385ede3d27e2eaf8b8c4ad8b2c29e3e52e","impliedFormat":1},{"version":"25fbe57c8ee3079e2201fe580578fab4f3a78881c98865b7c96233af00bf9624","impliedFormat":1},{"version":"62cc8477858487b4c4de7d7ae5e745a8ce0015c1592f398b63ee05d6e64ca295","impliedFormat":1},{"version":"cc2a9ec3cb10e4c0b8738b02c31798fad312d21ef20b6a2f5be1d077e9f5409d","impliedFormat":1},{"version":"4b4fadcda7d34034737598c07e2dca5d7e1e633cb3ba8dd4d2e6a7782b30b296","impliedFormat":1},{"version":"360fdc8829a51c5428636f1f83e7db36fef6c5a15ed4411b582d00a1c2bd6e97","impliedFormat":1},{"version":"1cf0d15e6ab1ecabbf329b906ae8543e6b8955133b7f6655f04d433e3a0597ab","impliedFormat":1},{"version":"7c9f98fe812643141502b30fb2b5ec56d16aaf94f98580276ae37b7924dd44a4","impliedFormat":1},{"version":"b3547893f24f59d0a644c52f55901b15a3fa1a115bc5ea9a582911469b9348b7","impliedFormat":1},{"version":"596e5b88b6ca8399076afcc22af6e6e0c4700c7cd1f420a78d637c3fb44a885e","impliedFormat":1},{"version":"adddf736e08132c7059ee572b128fdacb1c2650ace80d0f582e93d097ed4fbaf","impliedFormat":1},{"version":"d4cad9dc13e9c5348637170ddd5d95f7ed5fdfc856ddca40234fa55518bc99a6","impliedFormat":1},{"version":"d70675ba7ba7d02e52b7070a369957a70827e4b2bca2c1680c38a832e87b61fd","impliedFormat":1},{"version":"3be71f4ce8988a01e2f5368bdd58e1d60236baf511e4510ee9291c7b3729a27e","impliedFormat":1},{"version":"423d2ccc38e369a7527988d682fafc40267bcd6688a7473e59c5eea20a29b64f","impliedFormat":1},{"version":"2f9fde0868ed030277c678b435f63fcf03d27c04301299580a4017963cc04ce6","impliedFormat":1},{"version":"feeb73d48cc41c6dd23d17473521b0af877751504c30c18dc84267c8eeea429a","impliedFormat":1},{"version":"25f1159094dc0bf3a71313a74e0885426af21c5d6564a254004f2cadf9c5b052","impliedFormat":1},{"version":"cde493e09daad4bb29922fe633f760be9f0e8e2f39cdca999cce3b8690b5e13a","impliedFormat":1},{"version":"3d7f9eb12aface876f7b535cc89dcd416daf77f0b3573333f16ec0a70bcf902a","impliedFormat":1},{"version":"4304f640f7cb4724ea82441accb7c7607fa7207541182470d625adda99b2900b","impliedFormat":1},{"version":"e0205f04611bea8b5b82168065b8ef1476a8e96236201494eb8c785331c43118","impliedFormat":1},{"version":"62d26d8ba4fa15ab425c1b57a050ed76c5b0ecbffaa53f182110aa3a02405a07","impliedFormat":1},{"version":"9941cbf7ca695e95d588f5f1692ab040b078d44a95d231fa9a8f828186b7b77d","impliedFormat":1},{"version":"41b8775befd7ded7245a627e9f4de6110236688ce4c124d2d40c37bc1a3bfe05","impliedFormat":1},{"version":"f050afc4e2e063baf534e8bfa7aa6489e360f1016eba8603c19b45ba9fcd5887","impliedFormat":1},{"version":"9d6d5aec23fce486ccc52123d440d056519572f529d1f03dca71270d34efeec8","impliedFormat":1},{"version":"092657fbee8f216761a98d6d1242bfe819a5ba06d8f312eafb03fe8b9ba059ce","impliedFormat":1},"91347cfd6b895c282622c7697b9bf19eacd5b50407346d5c2f4aa831acca5a5a","84726d062eca1537b72beae8546a6d4d31b211249d7fa17f00365477b9c47932",{"version":"eac647a94fb1f09789e12dfecb52dcd678d05159a4796b4e415aa15892f3b103","impliedFormat":1},{"version":"0744807211f8cd16343fb1a796f53a8f7b7f95d4bd278c48febf657679bf28e6","impliedFormat":1},{"version":"aa34556c2db8ef26eb0a86c0214c4c4bafb78e6ae88a1bc746e74b2810497bcc","impliedFormat":99},"acae0280ca9bbe5a03938ef725156d6cf8fe98b1377397c845e1af3223293d93",{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"814d5c7384f3ca276e9dc4bcfde5545801a3ea0bfae09916b3336774e662fd1b","impliedFormat":1},{"version":"b838d4c72740eb0afd284bf7575b74c624b105eff2e8c7b4aeead57e7ac320ff","impliedFormat":1},"010a577ca2df8d1aa82d373d847e1c38e2afa5cab44990e2d7c52cb96bbfca2d","8f285c2f778f71a50116ebe67525c96946ef6b34c232268fcb51ea1338306461","d127ce453dc299575667b2d2df85aba60a4c7d5f02ea958a4723e0a95453cad7","afd8a4bbca53d8f088b0aa9a52836cdefab433d626f3a4ceae740ea68c528f03","10a7b3f9261ffcd6a575e064a9fcd698159bc3827070dd6a4381d1e3918bee10","7f5210b1b96b5e1fd2a44591b7c5f456a6344d3f9e77a665a7a81110a6b477e6","18df209ef31c31993ec0573a8358a0077c36d5f930e491fc0879e82d45faeda1","684318214628ab8205048fa94cee134b739424be2e9be92b4365987f37fa71ca","00b6e468e173fce37681ce37e73420458fd1a7d706e4d232d467715981f3e3a5","b6c5bf524fde0e77c472361ed9a8982e2e86fad887a4fc5edabd3cd77242bac2","8f92ca5f9a1c8c70c6655626a44d1275ed821bd829a58ec4393495bbe948222d","528d371e7f57a5ffc38101bea6a5f7309219c05add3345b643504ee13d46884d","f660bcf2fb73a20c5cf57f01d2086952053397b778a85de125b6417ea76ef71d","fac1899664c0fe90bf8211caaa113ae6d0f80478a826df642c536ab0b843afc4",{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"3671af8f58c05e9c83409ae56cb2a3db3c2e0a4cd082e685933d72f1c1a621ae","impliedFormat":1},{"version":"0f6d2591c6dbab10e42a87df2e9cfdf1673cb4001a03f32c75ad1a327329080a","impliedFormat":1},{"version":"37ecd7aa9e464b72ca1286207dc7567bff2214cd73e9da6bf9b5d3274707e210","impliedFormat":1},{"version":"173ab1860f3a46c49f123f1707e762b1e896ef7c96f834b73b273ed2243b7ab8","impliedFormat":1},{"version":"0cbdc76a71578cb1a06a59fbc0b42efe0a89aecbcf9924b55bccd969de595e8a","impliedFormat":1},{"version":"5cf5f73c2de135f142a5ad25ba6e47f035bd1706624693411c38e9e7be5b3cb2","impliedFormat":99},{"version":"21b8cf77735a940de185d9866f11bac0601efd24f5b5f126770c760758a42fab","impliedFormat":1},{"version":"c9734261912272d273de227f3ab615bbf2059489d57958d93c76f1247c27cd4b","impliedFormat":1},{"version":"ccb12d4724b5bb1078154b17b0a177afa836eeb93c210d96e1151d6f1d8fdb6d","impliedFormat":1},{"version":"c967e03c8dbb4770f7e2f0b71b5d13593de679a41cc2b60266c4b69f1342a263","impliedFormat":1},{"version":"843a93ea2fe50ec0daee21b1bf880a35e0cfac6a3294363596364fa0fb437e1b","impliedFormat":1},{"version":"c86a92cecb2e497a418b41a0a76f60e1306797f86db40e5261ef46a35ffa2f23","impliedFormat":1},{"version":"3849a38cf1dd97dce1da953542fd3b87853451e069ed4b581b77d8b3e2eb0336","impliedFormat":1},{"version":"e81484fc62d5e6add90882339bb2cdba0c87b85ca4002add438d0771ce2fdfa7","impliedFormat":1},{"version":"92ebc3261b20037c4e078cd3d26bccedb719b3eec653925e103b6ced4a936c0d","impliedFormat":1},{"version":"9acc441d14a127dea0228cd2645203c3285b296f452f723f850dc2941d2b9c7e","impliedFormat":1},{"version":"a4075b7a8211620f01d7a0cffb2d31fde9a2a6a108dec4cbaa3856b6a8e8864a","impliedFormat":1},{"version":"cee9e9bf9a8f67388ee878afd5e172189421bae6625076b774d68563d036fbfd","impliedFormat":1},{"version":"4bcfacf07ad3f040599fdede3807254c11c27d28f740206419c2d5354a571c72","impliedFormat":1},{"version":"7eb92baa673b920122e72e714caf84b78323758a3a214fb6383d717948143668","impliedFormat":1},{"version":"f37616d5f3b755ef9d2765218b06b933faf05cf094d18107cf4c50d81b44b6b0","impliedFormat":1},{"version":"c61e09e2a01aacd789fbcdbea4d386701422b8539ddc0285203d2a6bd0c4c1b5","impliedFormat":1},{"version":"3b78a632fd8d0490bf0eb5f8df1455e6f33028fb7c373d3d75275d06bfb6a7d9","impliedFormat":1},{"version":"d923dc7686f8a0bdabdbb0e8e61e6a95c403a3d6bc6f303af5381c9cd973ee43","impliedFormat":1},{"version":"da633553c8248c6ee21fd93a667d71ba4dcefc64f33632e3dc20ded5cbdd317c","impliedFormat":1},{"version":"050e8efc9defdf21d4c12a2ec280758c13ce66303d3e4e591d003089d99cbe4b","impliedFormat":1},{"version":"d924e653afba6a341ad72c3cbcc62a9c1206d5fcdf02db13fded64bfbca27e87","impliedFormat":1},{"version":"5d1201e776c3167527653c835035e4ad29cd79e0d6b139aa250ca74899e0741e","impliedFormat":1},{"version":"2cb6713aff9ae0ff160ea7cf286806b6a4400f40aac4ae675fa3079b0e60a9c3","impliedFormat":1},{"version":"d14f95b26d56db055148411a3010f2b449b90bc07f7a179dee0620b36b0bb3bf","impliedFormat":1},{"version":"1fdf5c750e4164249aaa3095803330eae7cc9fb2523535811800460b98f8e7ed","impliedFormat":1},{"version":"8674e77147967c8f75aaa22923ebc836dd7620ee0cf52bbe91b89114f8d91413","impliedFormat":1},{"version":"9f4ef6fd452db4c4d5f96293732ee29c03f54755744342809dea96f63fd7227b","impliedFormat":1},{"version":"57cdb6dba0f7f107cd3ec872e52916ea2901c9a80611e7e669c2ccf3a2219f17","impliedFormat":1},{"version":"20d246417a79b06bca6fe01426258a3408068442899b990472e521eafd6ac5b4","impliedFormat":1},{"version":"c3f937028caf49d383b109a93128164de319c1a5ec3796c02da60acb580e1e9a","impliedFormat":1},{"version":"e6b92657c5108dd00f77e51cff0d522a10f93a900d75597440c50bb64d4278bd","impliedFormat":1},{"version":"8a60ed93d81f472e270e213c5da23bdfc2a87b6616031f4d397aced25f727217","impliedFormat":1},{"version":"5f2b95921cc6b959e8ca7abc17943382f7e5fe0ea6ef36c5b8dc383def96b1f8","impliedFormat":1},{"version":"43006ce2de2caf33f0e26d937195d197e8b91af1222a1b24532daa3915446c86","impliedFormat":1},{"version":"006577276d8f3b0012b4f856662618082910ed31a71464a42753692be92e4a2a","impliedFormat":1},{"version":"58004a9240ee74db43ce3ab2343cc29473e969adcd592c6fce46939d94512d93","impliedFormat":1},{"version":"492409753b45983851b6d66272f384bcb2dfc045d48eb07e8c8998a571495e63","impliedFormat":1},{"version":"2db60104bde79eac5c47dcfa9738246190173cb76966d88e42959ca8d1ea7e27","impliedFormat":1},{"version":"1fa946b4013f86f5a90107c6cf08850edd290a11b8b400fe3a4c7c24bc800785","impliedFormat":1},{"version":"2105f9dfb4de768168ca5234b185aa508f4a9e9ecdd937400e05fb52734e49c5","impliedFormat":1},{"version":"9e0b7af2247ab847874dc5ca0a92c4f28f55332b8241591bd06fafd3d184f605","impliedFormat":1},{"version":"39bff71bf16f3a020c438f5ddc1a24ab26c28dad91d324372eabbce88abaec74","impliedFormat":1},{"version":"5a395ff9e24f89cc08679e731889b2ed1bfe451ba76f16184ccef0b742589d13","impliedFormat":1},{"version":"0651a8dd2c6446154e0994391f7bdebbde389dc7ec75ac4a0f727fff5255143c","impliedFormat":1},{"version":"2088a7c3bf5a885904de841f5fa6103d8689e439a3cb3273f3bac69c1b3a3b1b","impliedFormat":1},{"version":"6dbc5313fe49ecbab3215f1cb1733d7348b392f1ca12c331c5720f4ea0036f47","impliedFormat":1},{"version":"3ed4ef1f210705e2c320e5b05787d7b6e74b7920492a76bb8712857bb22fc915","impliedFormat":1},{"version":"6fca2337de679c9c118e9005f3ee7f41725690a923bbff4ee20401e879471acd","impliedFormat":1},{"version":"58f59363f3c50919bdc19c44e68b35bb471548486ca98f6e757de252d5d1e856","impliedFormat":1},{"version":"109381191d7b0beb0de64a68ce3735fff9c91944180bfb6abfe42080b116689b","impliedFormat":1},{"version":"b04f68c5b937801cebf5264072a6f4a1f76050a75fd0830d65ae0bf0275ed1fc","impliedFormat":1},{"version":"ad42060f3e0f92a294748f19d9490a8a6a980fb40dda0fd4627991d1361862cc","impliedFormat":1},{"version":"8c00c7abd1e6e594cb9726674fdc65e4d9681d1e4c890bdbd602096e3b48f917","impliedFormat":1},{"version":"ce6b390be6cdd541f54e393b87ce72b0d1171732f9e93c59716e622a5b2e3be5","impliedFormat":1},{"version":"5aa50acb079a18441d0984acda7d3dbbc66a326fccacb20a75d836e797bc8b80","impliedFormat":1},{"version":"6735eae673357ba7f9fc7e55af3b00e1415b32d3b639c38fb936151f336a5978","impliedFormat":1},{"version":"386ff073cfe770b93867e65c26e969d672aeb42fc5506279c71a0185fd653539","impliedFormat":1},{"version":"e967582e89f2a455eafd8bf1232dd81ee207709a48c07322e996ecb0672148bb","impliedFormat":1},{"version":"25528369e718c89acd957ae0e72b1b5105b1111329d31442d8d639ee020b3fce","impliedFormat":1},{"version":"8764a0ff3269684a2c85a54acd7e90d33876927140e28880b8a4c95e8ca63bd6","impliedFormat":1},{"version":"e4ff6f622aea4c2bd44136810e979cd2b8ae41fb94b8d72246a1a449a0146266","impliedFormat":1},{"version":"ea09e3f830cb4da7a144e49803ebd79ad7871e21763fd0a0072ab8fb4aee43b5","impliedFormat":1},{"version":"02cbdc4c83ba725dfb0b9a230d9514eca2769190ea7ef6e6f29816e7ad21ea98","impliedFormat":1},{"version":"228eca402015a3ac3ee5b0726de796244ab2b4db866c2294983b8027ade23338","impliedFormat":1},{"version":"f1e533f10851941ccd2ee623988b26b07aecb84a290eb56627182bc4ca96d1a8","impliedFormat":1},{"version":"5d89916c41cc7051b9c83148d704c4e5aa20343a07efd14b953d16c693eda3ee","impliedFormat":1},{"version":"06124be387e6fc43c6a5727ecb8d6f5380c52878341a2cd065dc968e203029e0","impliedFormat":1},{"version":"44c575e350e5b2c7771137b2797eb3d755b67dd286622158a3855487a6182253","impliedFormat":1},{"version":"a088d5ba9a4fa3a96bcda498268269d163348229c43187950a9b2b7503d46813","impliedFormat":1},{"version":"cf5408ade74fb2ec127a10bb3b1079a386131818bc7ac67a002c4a6c3ec81b62","impliedFormat":1},{"version":"6cf129a29ce866e432f575c5e4c90f44f2fb72d070b9c3901acdb3cbb56fa46d","impliedFormat":1},{"version":"8af2fead6dd3a9cd0471d27018dd49f65f5cc264c4604a11aba4e46b2252eb89","impliedFormat":1},{"version":"677c78ed184c32e4ff0be1e4baf0fbf1a0cccd4f41532527735a2c43edd58a87","impliedFormat":1},{"version":"70415c6e264d10d01f7438d40e1a85b815ace6598e4a73f491b33db7820e1469","impliedFormat":1},{"version":"38fa05ec45e9bddcb55c47b437330c229655e3b0325b07dd72206a10bf329a05","impliedFormat":1},{"version":"8b11a987390721ea4930dcc7aca1dec606a2cd1b03fb27d05e4c995875ee54bb","impliedFormat":1},{"version":"3b05973f4a6dc88d28c125b744dc99d2a527bdb3c567eda1b439d10ce70246f5","impliedFormat":1},{"version":"2ee3f52f480021bd7d23fe72e66ba0ec8d0a464d2295ab612d409d45a3f9d7ae","impliedFormat":1},{"version":"06e13dc31517e5123765206ad170f0625b8a1a1321f702598a0563a9ac7d68c3","impliedFormat":1},{"version":"c56439d9bf05c500219f2db6e49cd4b418f2f9fb14043dee96b2d115276012b8","impliedFormat":1},{"version":"f1af782c9107979097db42a2e6746e7cea7a2578ef36c92015e5422fb2838b98","impliedFormat":1},{"version":"9811146d06f6b7615165f0dcd3d2aaea72adb260c8e747449b7a87c4c44f7ff1","impliedFormat":1},{"version":"b4e618b2d4422fa5fae63e999dccb69736b03ec7b0c6fd2d4dc833263d40921c","impliedFormat":1},{"version":"21a06a5d3e4f859723386772d4c481ed5b40f883ecd4ed9a8ec8bcb54a10e542","impliedFormat":1},{"version":"55f4ba9c75263294a97f3d253bf295ae94fd519f544eda4af3cc12b19bc75a66","impliedFormat":1},{"version":"5ef6b0404100d30e3b47c73021f2da740d1fa8088fda5adc741706cb3e73cf13","impliedFormat":1},{"version":"24081c38e738a14e6d002c166ba297a9656cd8b18475b36bc7c99bb55c7d5cca","impliedFormat":1},{"version":"d1342658b16b92d24b961db5c1779dc03fe30194fd6fea0d15dc8e946f82d83f","impliedFormat":1},{"version":"cbd4ff12f799a44b629643edc686aeec830fbb867c69cb6609da57d205057717","impliedFormat":1},{"version":"4f4d1284bc93168a1a0b2888f528aa689828917cdc547802ab29c0d1f553be40","impliedFormat":1},{"version":"fd15b208613892273f0675f55b31c878e22a28d62d306e589867009592f67166","impliedFormat":1},{"version":"ef5bc836c5c0886cd8c9cf1cff6192f4f1e82ef1f8088c9f136586b9860051e0","impliedFormat":1},{"version":"41772d8304e6429b825dced302b65ace7ed6fb9dd03e3726fe6230821735ca38","impliedFormat":1},{"version":"001fc9e5e2a353521cc0807e759f7c5a88cc18a8389568cf94b8809c38f00cd4","impliedFormat":1},{"version":"d14cd6c9001dfa6f96660952945c344370109247764ab42b47d110fcbff678e7","impliedFormat":1},{"version":"282c7f230b9d2fda119b5b4a9567fae09ea09ea1bbacaa2085ef622d217979d8","impliedFormat":1},{"version":"4db00e3ce9cd4d68249907352b1f6c41c687b58f088bc2c8bff1bc41800bb732","impliedFormat":1},{"version":"316b2ea3cb57f2ccf86a19c4016eebb97eb177f3d5b5dc4b3d0508439afffff0","impliedFormat":1},{"version":"71de65e470fb5a0920472a8b13d37fff8960822e34d709aee14599802c15770c","impliedFormat":1},{"version":"c0cbe98c4e104042383444c718d2ce2d0dd602e6b7d52dc3185bbdf289da1128","impliedFormat":1},{"version":"c3c8297d66976e60076da541ec418590bf26d1056980b9adfea2c14baaf2089e","impliedFormat":1},{"version":"17ec351733c9b9a5de7d0aee5f710ca792a19efc365bed93ec045b885c309fde","impliedFormat":1},{"version":"8bb061c812d97dedb8549ca46cd3b8bae3f2494ef681d9712c64c1b933801ebf","impliedFormat":1},{"version":"969ab03feed7516ece5c6c0468e6c39391ed75317dd641d5600736b131559ad6","impliedFormat":1},{"version":"bad5090a58412608fea6b3ec9b239a7e543a0783d91e41069d446d014000c4a1","impliedFormat":1},{"version":"4d9a748614547079332206f71cfc4c473a22cf834e78632e014e159884dfbf32","impliedFormat":1},{"version":"89c710eef54f9726d13eb123a800285d9b5cf2eb64d98f4c3a7b0e5a162ad24f","impliedFormat":1},{"version":"a97990e77a23aea39060610aef4b4bb92154d5330ecb0b557324ba4c14a1db41","impliedFormat":1},{"version":"d2b89296b175b0a1a11ce09cc682e6f86b24d34abd1bdf8c932a82c4e99b551a","impliedFormat":1},{"version":"0f53b7d860cf0b14fc6bbad4c6c9599196e2a39362adc49e57aba696e95ae57e","impliedFormat":1},{"version":"cb17678275c05a8ee81809adc025132d681b80b1625671d66fdd7625d6778c8f","impliedFormat":1},{"version":"b90283ab6c36fc580b06cb293629a9b37eaba24e17ff9ae2f0d874a3f3a962a1","impliedFormat":1},{"version":"c1425155d2396f10be607f43392284b6bfc98b542bb49c611eaa2038b6a72112","impliedFormat":1},{"version":"30e0e58b2b36491323f748cc938b93eba059d354abecee659ba0e9312a842a5d","impliedFormat":1},{"version":"c2d8eccfe4adada4730bbd4f2568627d5d4aeb27cfbc8d39aa974ce33e855977","impliedFormat":1},{"version":"21d0cc7ad656b0348bfd745fb598399c6f9531ffef6ff1b8996fe42c5f185f0a","impliedFormat":1},{"version":"e65828a891223557cd959641689251fa5c76b90c4a0ceea179394effd2113d3e","impliedFormat":1},{"version":"b5bbf77bea59350deb48316308ae673afa70a77ffc86edc1e707af94a4b2a1d2","impliedFormat":1},{"version":"6eddc1432777582b4797eb53c43b9917b1ba8908a737f7823a7049620f98588b","impliedFormat":1},{"version":"79e7eb72b4d9ca2d268460d35fa7bfe01db96e93659752bd5fc5cbf5c5be8294","impliedFormat":1},{"version":"10ad4c890e509380deb83c8bec650899df9bd70ee20238f2221d6bdc36043a0e","impliedFormat":1},{"version":"1a3b837513da5afd3bb0b228dab3a089fce405344243e372672f641ededf9b48","impliedFormat":1},{"version":"901f6b020440eac80a83a7ca248ca244e2a296be6b1ed8645a884a4509e11fc7","impliedFormat":1},{"version":"bde8d307f9b1b9d561b8a5af91ae8bae5fd1c6b3f11bc6187e1bb9c69ef12f79","impliedFormat":1},{"version":"98cd13b7db22a532d647b8126c0cefa1b1227c867e08fecd9e57a6b519f178ea","impliedFormat":1},{"version":"1c9035abc7efd7aab387756979d801790769fcde2bdf03132fc2f924a9733e2e","impliedFormat":1},{"version":"8cc0489a9c22e8d213fc993043074464eae269074ca2362967096f64538b07b5","impliedFormat":1},"1c61213808c0174679ecd1128420a34f3f61dd02b5f94135a2168a0d4514b827",{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"4ead13a482c539b77394b2a97e3b877b809eac596390371cea490286f53b996a","impliedFormat":99},{"version":"06db2f8ba1d1dfacf04529cb731081ab23f133f29c7608ebdfbcab356996827c","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"3a582c6e8906f5b094ccf0de6cc6f4f8a54b05a34f52517aba5c9c7f704f6b28","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"49ab4f1d153a252779958fc87b700743d32b5ffa42addd70ae23ad3f429daa5c","impliedFormat":99},{"version":"0528f6d21f7a02d4092895090d2dd86104bd5a3e79eced96d5a1a7dd90943d17","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"5c935b7fc4ddc1410ea1cd7cd4e35ed106a6e4920dd27a9480a40fd224359dc3","affectsGlobalScope":true,"impliedFormat":99},{"version":"b5ce343886d23392be9c8280e9f24a87f1d7d3667f6672c2fe4aa61fa4ece7d4","impliedFormat":99},{"version":"72ce5b734c05da85c85a6f6dc05823b051d6aa41acaedeeb1d17c72f3b4efa72","impliedFormat":99},{"version":"b0857bb28fd5236ace84280f79a25093f919fd0eff13e47cc26ea03de60a7294","impliedFormat":99},{"version":"5e43e0824f10cd8c48e7a8c5c673638488925a12c31f0f9e0957965c290eb14c","impliedFormat":99},{"version":"53cf4076f42b29b8d411259d168d51b3a0274c42c8814e5b44dfa8803a35d4fc","impliedFormat":99},{"version":"a39461ee1f27cf3e6cfd63d21045713d26d521da55ea4d8efccb705f689e6dbb","impliedFormat":99},{"version":"61bb64660ee150f3ab618340e15cca0a81664801bede7c966ca0eca3a952fe63","impliedFormat":99},{"version":"42a12f2faa483c9b48195ed794d22698162274e755f6e07219c2351c4f08d732","impliedFormat":99},{"version":"ec0c42bb0f465e4993f2bc68a6ce9df9a2dcbc7b83e21748f82f1b69561938e3","impliedFormat":99},{"version":"f50ff37a9cbbe74475f426474d9827083c7c2c138a954d28f1690df338f69291","impliedFormat":99},{"version":"61fd6c17235d530c40f543dd7c40afab091d91c1ef890baeed30db6d82b04b28","impliedFormat":99},{"version":"bcbd3becd08b4515225880abea0dbfbbf0d1181ce3af8f18f72f61edbe4febfb","impliedFormat":99},{"version":"091767bc841f937654ed597d49e023ed59850355e746ae1a6f20ab31076ee1fb","impliedFormat":99},{"version":"19c6d6135af59693698d384050b45a8a049493500add442f58e4bd7c8a255ab6","impliedFormat":99},{"version":"6a0dba12d55314638a8c51108b20fe2f68f1364a619d098918bda91c22dec154","impliedFormat":99},{"version":"8124828a11be7db984fcdab052fd4ff756b18edcfa8d71118b55388176210923","impliedFormat":99},{"version":"2b39c6cf59088713babbfc3e20ee85f1375d40e66953156fa658346b8346f24f","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"6987dfb4b0c4e02112cc4e548e7a77b3d9ddfeffa8c8a2db13ceac361a4567d9","impliedFormat":99},{"version":"5e2ba3d18d78aebbde1f34bde356e41e9c76eeaeaeee56a37036596a9eff4211","impliedFormat":99},{"version":"8280ae8ccc0493b32d1742d585357ab9f0a508ea050af25a5a20d64010d0a5cf","impliedFormat":99},{"version":"7adfd9f9056ecd4ae6c65fde2a98654960c662714c73f048478959d04c09e144","impliedFormat":99},{"version":"437b7613a30a2fcde463f7b707c6d5567a8823fbc51de50b8641bf5b1d126fad","impliedFormat":99},{"version":"63ea959e28c110923f495576e614fb8b36c09b6828b467b2c7cd7f03b03ccf9f","impliedFormat":99},{"version":"1601a95dbb33059fc3d12638ed2a9aecff899e339c5c0f3a0b28768866d385b4","impliedFormat":99},{"version":"56fc978580577d30f4c2cdb5b1eb9217b66ed66537dd27141256f426e4b8dd68","impliedFormat":99},{"version":"2c5413050a2580becf9d82dd7e3006b95623e96f145356bf73230cd635352f70","impliedFormat":99},{"version":"860bedc71ead192ea4a0ea5ef4686e65724d14b391ebd1a6671a7044e6bd8e15","impliedFormat":99},{"version":"7c0a845bee4a084cbb8654709f48e5f13e2f6d45e5e2dde7c57cadf79fd9e3d5","impliedFormat":99},{"version":"7fa6fdc6eeeb3850e6b3166836f8c9199594c4f4bed3cee02cac0ca33d71894d","impliedFormat":99},{"version":"e53757f3e6688e8be16b36ccbeedc85637e607353fc67ff7b6665d6005aeaa12","impliedFormat":99},{"version":"88ba2660f5b024c7afd02ea700bc0c677687e17e34f0a94c3256ddb55e0f1c5d","impliedFormat":99},{"version":"7bbff6783e96c691a41a7cf12dd5486b8166a01b0c57d071dbcfca55c9525ec4","impliedFormat":99},"bb5494b0eb39fc58cb660d458b4008f4616ed69c0db021c1682f935376aab0a7","cb7f7f317786126675c00d185ab4f1b86cc22a9f69a9204d3d52f3e97898bf0d","1b680285edb90900758fbe8cef6fd1cb134d369a5b7023d9217cb7faa7282858","3bc29e8b8229ad7982cd5ce6bde018225114888a03e0078db7e971b4fd66510f","06c1ae07f810759bad44a7536d7c2b60e28420d7fd3ebd1b07d449ad32c9c2ea","4d8fda46772d3d906a222dd6b21bf4a648311cffebf7b2ac542a34661c7eb088","1e08f7532eaac596896613ce259b35efe3327cb33af4f7c775f149fe30886ed3","a29e9252725bef468dac5ab910896177fc2bb702c1266c50fa54fc353898019b","30686c2c17651c64437cdf6b0627183ee55834904499d16d7ca24e908c50dcb7","76e2d2124da36172f48a9941a768c97f38ab82d64a4da01aa115ff1a36927553","9114c7728a2a397e5103d7bcc90e2518b5fec5e4fc58caa06841a2979e0e3bf2","7abbbad29d5161aaceddcd40ad06d1c533fab7a78b29ea241840a24d50269ae3","84554d9d03d88f28fdb5b634741da19f542426f72da4a27955217572e17b89d6","cfc7d71f9cdf1e86cd80ecb3578e6948c6e55cc8f774c7591ebfe60bb506f0da",{"version":"8f82ea3a953aadf71d6de753793c1dbe91d7989f092a9b4f471764af11470b76","impliedFormat":1},"7445185d6424e574027a7cd14029e000784eaa03a2b83ab097742079f701e702","7bf1fc5fd4df8f975565e85e1b7d2eadecbe50f2aec1f85ab2893b65b028e6b9","e394910d3e1c46d222e06aeeeda497e0018be3c8ff4284b007a1e5e81aa827ca","3dbd02e9eaf35410ffb07d7597ccd7a46cf309f772d0ad07912233c63bcdf28e","7b97275e9cfeeaca9870c974e2af2238c1f4f3d32433bb5d17489f3d050f9f1e","2ddde070d923e39a58f7fa389c05d65cea49cc2bddb6d62825c45450d8d18a4f","69cd1e2ca8f9659824707e032bb95943ce44c86cbc377dfb811c60a404b543c7","563cf623b7d1bd8a57fb7c0133a22c46d03f24af56d431fa3274071577bbd837","644137458ffd1d507051558432388065812a481cb6c7b84a421c34882f63d9fb","63eb644e25f36589c0c8d530f363e1ae6794ce377425b9a4cad897c12a9d85fa","2750ab903da8aa458fccc6dbeab5949b9b6ef5a6d9af4bd18a4461727c009e89","1d414aa555f096ac9db1385e30c8268842eb4b41f123e56f9a7d8d308da73d22",{"version":"4ffba3c5848b4fe62ee59b754fd5f256ad9656a0db6d37b9a2a8cb40dfc7ac21","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"32b35cf0dc3a1b1a7118b61c34ce2ad1a29695851679f9ec34e0776f2ece2a69","impliedFormat":99},{"version":"b413fbc6658fe2774f8bf9a15cf4c53e586fc38a2d5256b3b9647da242c14389","impliedFormat":99},{"version":"59e5e964b84fdb2378e9455e4e59405030e4ed2b4c6f891ce395f17796af3cbb","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"90ba95a763101bb61b8a799731a2ed60b5016b8135c1a2d5186862d4b534d4a1","impliedFormat":99},"a24d4932ea0e71cb191d8f345f90cab049b29ee19cef3979188a7a3ecc60144c","358a6fbe33c9e11c2836d7b2530e59eddf4347356dfc10baf70922aa7a6409e0","d973481b96e3e66bd3464abc42f58518db57d35f6d44e1959106909197b1ba54","f755342a97aede2d2f5fbe937536e34a4109541beb74af283d8d9675bc9b7060","051918a3041ed8b07d399e4afdea7b6c181c76cb9fb1436fc6e5a31e79333a35","84a80e71409c082c632aee7930ec3228c35cea49f99fc0afdc34e6147ccc5565","00f37a65a031c3db2a0c027f70e2d6357bdcd2e326eec03c8a9073395fc12a23","2050392e047b0fdc18251e50b5758ecf7867ba917a685d77c28802548cef1e43","7f95e7381ad8cd4ffd7dfb11a3f2948e401d6d37e3b8356a74bf5e27049db175","8f260abb16b878fd0b3e4e348e9d240b272bffee4947d31957467983ac86ca04","9ccfc9de5a39ee4c087d6939c97c54533017b449ff4e7db5de2917b23e5d34d9","6989c253a92bcc82180e0d9060c5e3a73e2ea10b2edaa433cf834106da1c1342","00bb0e7e01e3d13eb736dc09cb71c02dc0d2dce6ed0b36b278ab2bf8d3fedc9d","0cf931c0325581b6d64ff6e57ca7dcae151f99a04f3f802cc882d5107ce968f0","55ab9bb80a3163a90fae5863ea336b2ebb416bf3987f6ac58c67a6958e973a1c","7716f5bd6fc4f452901518702e2470a29e0d7c8cb37aa5a27a3c41ae9da91bf5","518c6ae195a899ccbbbe2d5a415cf75b8587c4533658ae05957fa393592f393f","5df77a846a4833457a2b4fbcde68c58689d95e34dc29de51d63279cdfc3bc8ce","db58fe69f5e869945e7532628ca74786a9ba802a370bbeecf49e3bc256c8c639"],"root":[319,320,342,478,[524,526],[540,547],550,[562,580]],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99,"skipLibCheck":true,"strict":true,"target":9,"tsBuildInfoFile":"./node.tsbuildinfo"},"referencedMap":[[478,1],[350,2],[345,3],[349,2],[347,4],[346,5],[190,6],[192,6],[193,7],[269,8],[268,6],[247,9],[244,10],[243,11],[245,12],[246,11],[242,6],[482,6],[489,13],[487,6],[343,6],[344,6],[123,14],[124,14],[125,15],[61,16],[126,17],[127,18],[128,19],[129,20],[130,21],[131,22],[132,23],[133,24],[134,25],[135,25],[136,26],[137,27],[138,6],[139,28],[140,29],[62,6],[60,6],[141,30],[142,31],[143,32],[185,33],[144,34],[145,35],[146,34],[147,36],[148,37],[149,38],[150,39],[151,39],[152,39],[153,40],[154,41],[155,42],[156,43],[157,44],[158,45],[159,45],[160,46],[161,6],[162,6],[163,47],[164,48],[165,49],[166,50],[167,51],[168,52],[169,53],[170,54],[171,55],[172,56],[173,57],[174,58],[175,59],[176,60],[177,61],[178,62],[179,63],[180,64],[181,65],[63,34],[64,6],[65,66],[66,67],[67,6],[68,68],[69,6],[114,69],[115,70],[116,71],[117,71],[118,72],[119,6],[120,17],[121,73],[122,70],[182,74],[183,75],[184,76],[325,6],[327,77],[328,78],[363,79],[364,80],[437,81],[390,82],[391,82],[401,83],[389,84],[388,6],[392,82],[393,82],[394,82],[402,85],[395,82],[396,82],[397,82],[398,82],[399,82],[400,82],[438,86],[436,87],[433,88],[403,89],[435,90],[434,91],[432,92],[430,93],[415,93],[428,93],[416,93],[417,93],[418,93],[419,93],[420,93],[410,94],[421,93],[411,95],[431,96],[422,93],[412,93],[427,97],[414,98],[409,6],[423,93],[424,93],[413,93],[425,93],[426,93],[429,99],[405,100],[407,101],[408,102],[406,103],[404,104],[355,105],[359,106],[356,6],[357,107],[358,108],[360,6],[361,105],[370,109],[371,110],[372,105],[384,111],[373,112],[374,113],[375,114],[376,6],[368,115],[369,116],[380,117],[366,118],[367,119],[365,109],[381,6],[382,6],[383,6],[453,120],[458,121],[454,6],[455,122],[456,120],[457,120],[459,122],[462,123],[460,122],[461,122],[463,6],[464,6],[465,120],[469,124],[466,125],[467,6],[468,126],[473,127],[441,6],[386,122],[446,128],[450,129],[447,130],[448,131],[449,132],[452,133],[444,134],[440,135],[387,115],[445,136],[443,137],[451,138],[439,139],[442,140],[385,141],[472,142],[470,6],[471,6],[377,115],[379,143],[378,6],[236,144],[235,6],[490,145],[511,146],[512,147],[510,6],[483,6],[496,148],[495,149],[507,148],[498,150],[500,151],[519,151],[499,152],[480,153],[479,6],[485,154],[486,155],[516,156],[492,157],[494,158],[515,6],[513,157],[493,6],[484,155],[491,6],[316,159],[318,160],[317,159],[315,161],[272,6],[274,162],[273,163],[278,164],[313,165],[310,166],[312,167],[275,166],[276,168],[280,168],[279,169],[277,170],[311,171],[309,166],[314,172],[307,6],[308,6],[281,173],[286,166],[288,166],[283,166],[284,173],[290,166],[291,174],[282,166],[287,166],[289,166],[285,166],[305,175],[304,166],[306,176],[300,166],[302,166],[301,166],[297,166],[303,177],[298,166],[299,178],[292,166],[293,166],[294,166],[295,166],[296,166],[321,6],[488,6],[322,179],[326,6],[351,180],[352,181],[348,182],[558,183],[560,184],[559,185],[557,186],[556,6],[271,6],[353,6],[538,6],[539,187],[223,188],[221,189],[222,190],[210,191],[211,189],[218,192],[209,193],[214,194],[224,6],[215,195],[220,196],[226,197],[225,198],[208,199],[216,200],[217,201],[212,202],[219,188],[213,203],[232,204],[195,205],[196,206],[199,207],[191,208],[198,209],[194,210],[189,6],[200,211],[201,212],[262,213],[240,214],[263,213],[264,215],[265,215],[251,6],[252,214],[253,214],[254,216],[255,216],[256,217],[257,217],[237,6],[258,217],[249,218],[259,214],[238,214],[260,217],[241,215],[248,219],[239,220],[266,221],[270,222],[250,223],[267,224],[261,225],[207,6],[508,6],[481,6],[474,6],[475,226],[476,6],[477,227],[58,6],[59,6],[11,6],[10,6],[2,6],[12,6],[13,6],[14,6],[15,6],[16,6],[17,6],[18,6],[19,6],[3,6],[20,6],[21,6],[4,6],[22,6],[26,6],[23,6],[24,6],[25,6],[27,6],[28,6],[29,6],[5,6],[30,6],[31,6],[32,6],[33,6],[6,6],[37,6],[34,6],[35,6],[36,6],[38,6],[7,6],[39,6],[44,6],[45,6],[40,6],[41,6],[42,6],[43,6],[8,6],[49,6],[46,6],[47,6],[48,6],[50,6],[9,6],[51,6],[52,6],[53,6],[55,6],[54,6],[56,6],[1,6],[57,6],[362,105],[354,6],[88,228],[102,229],[85,230],[103,231],[112,232],[76,233],[77,234],[75,235],[111,236],[106,237],[110,238],[79,239],[89,240],[99,241],[78,242],[109,243],[73,244],[74,237],[80,240],[81,6],[87,245],[84,240],[71,246],[113,247],[104,248],[92,249],[91,240],[93,250],[96,251],[90,252],[94,253],[107,236],[82,254],[83,255],[97,256],[72,231],[101,257],[100,240],[86,255],[95,258],[98,259],[105,6],[70,6],[108,260],[323,261],[187,262],[234,263],[203,264],[227,6],[229,265],[228,6],[188,262],[186,6],[202,266],[233,6],[231,6],[204,6],[230,267],[197,268],[206,6],[205,269],[518,270],[523,271],[517,272],[509,273],[505,274],[501,275],[514,6],[502,150],[554,276],[551,277],[521,278],[520,279],[503,280],[553,281],[497,6],[504,282],[522,283],[561,284],[555,285],[552,6],[506,6],[324,6],[525,6],[546,6],[549,286],[336,187],[338,187],[337,187],[335,187],[341,287],[334,187],[333,187],[330,187],[340,187],[339,187],[331,187],[332,187],[329,187],[548,187],[529,6],[531,6],[530,288],[527,6],[532,6],[528,289],[535,6],[534,6],[537,290],[533,291],[536,292],[562,293],[541,6],[564,294],[563,6],[565,295],[543,296],[566,297],[542,298],[540,299],[567,6],[568,300],[544,301],[320,302],[570,303],[569,296],[550,304],[571,305],[547,306],[545,307],[574,308],[573,309],[576,310],[575,311],[577,312],[572,6],[578,313],[319,314],[580,315],[579,316],[526,6],[342,317],[524,318]],"semanticDiagnosticsPerFile":[[320,[{"start":36,"length":19,"messageText":"An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.","category":1,"code":5097},{"start":4076,"length":7,"code":2345,"category":1,"messageText":{"messageText":"Argument of type 'ValidationProblem[]' is not assignable to parameter of type '{ name: string; errors: string[]; data?: T[] | undefined; }[]'.","category":1,"code":2345,"next":[{"messageText":"Type 'ValidationProblem' is not assignable to type '{ name: string; errors: string[]; data?: T[] | undefined; }'.","category":1,"code":2322,"next":[{"messageText":"Types of property 'data' are incompatible.","category":1,"code":2326,"next":[{"messageText":"Type 'unknown' is not assignable to type 'T[] | undefined'.","category":1,"code":2322,"canonicalHead":{"code":2322,"messageText":"Type 'ValidationProblem' is not assignable to type '{ name: string; errors: string[]; data?: T[] | undefined; }'."}}]}]}]}},{"start":4114,"length":6,"code":2322,"category":1,"messageText":{"messageText":"Type 'unknown[]' is not assignable to type 'T[]'.","category":1,"code":2322,"next":[{"messageText":"Type 'unknown' is not assignable to type 'T'.","category":1,"code":2322,"next":[{"messageText":"'T' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.","category":1,"code":5082}]}]}},{"start":4156,"length":6,"code":2322,"category":1,"messageText":{"messageText":"Type 'unknown[]' is not assignable to type 'T[]'.","category":1,"code":2322,"next":[{"messageText":"Type 'unknown' is not assignable to type 'T'.","category":1,"code":2322,"next":[{"messageText":"'T' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.","category":1,"code":5082}]}]}}]],[342,[{"start":147,"length":8,"messageText":"Cannot find module 'rollup' or its corresponding type declarations.","category":1,"code":2307}]],[547,[{"start":6038,"length":17,"code":2322,"category":1,"messageText":{"messageText":"Type '(string | number)[]' is not assignable to type 'string[]'.","category":1,"code":2322,"next":[{"messageText":"Type 'string | number' is not assignable to type 'string'.","category":1,"code":2322,"next":[{"messageText":"Type 'number' is not assignable to type 'string'.","category":1,"code":2322}]}]}},{"start":15175,"length":4,"code":2345,"category":1,"messageText":"Argument of type 'string' is not assignable to parameter of type 'CountryCodeV1'."},{"start":18883,"length":18,"code":2345,"category":1,"messageText":"Argument of type '\"dataAvailability\"' is not assignable to parameter of type '\"emissionsTrajectory\" | \"policyAmbition\" | \"pathwayType\" | \"geography\" | \"metric\" | \"modelYearNetzero\" | \"modelTempIncrease\" | \"sector\"'."}]],[549,[{"start":610,"length":4,"code":2339,"category":1,"messageText":"Property 'glob' does not exist on type 'ImportMeta'."}]],[550,[{"start":593,"length":112,"code":2345,"category":1,"messageText":{"messageText":"Argument of type 'OptionToken' is not assignable to parameter of type 'number'.","category":1,"code":2345,"next":[{"messageText":"Type 'string' is not assignable to type 'number'.","category":1,"code":2322}]}},{"start":813,"length":108,"messageText":"The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.","category":1,"code":2362}]],[566,[{"start":4484,"length":1,"code":2345,"category":1,"messageText":"Argument of type 'string' is not assignable to parameter of type '\"AD\" | \"AE\" | \"AF\" | \"AG\" | \"AI\" | \"AL\" | \"AM\" | \"AO\" | \"AR\" | \"AS\" | \"AT\" | \"AU\" | \"AW\" | \"AX\" | \"AZ\" | \"BA\" | \"BB\" | \"BD\" | \"BE\" | \"BF\" | \"BG\" | \"BH\" | \"BI\" | \"BJ\" | \"BL\" | \"BM\" | \"BN\" | ... 220 more ... | \"ZW\"'."}]],[579,[{"start":2080,"length":25,"messageText":"An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.","category":1,"code":5097}]]],"affectedFilesPendingEmit":[478,525,546,549,341,537,562,541,564,563,565,543,566,542,540,567,568,544,320,570,569,550,571,547,545,574,573,576,575,577,572,578,319,580,579,526,342,524],"emitSignatures":[319,320,341,342,478,524,525,526,537,540,541,542,543,544,545,546,547,549,550,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580],"version":"6.0.3"} diff --git a/scripts/codemod-v1-to-v2.test.ts b/scripts/codemod-v1-to-v2.test.ts new file mode 100644 index 00000000..c07527de --- /dev/null +++ b/scripts/codemod-v1-to-v2.test.ts @@ -0,0 +1,346 @@ +import { describe, it, expect } from "vitest"; +import { + splitExpertOverview, + widestScope, + upgradeV1ToV2, + PATHWAY_DESCRIPTION, + CORE_DRIVERS, + TRANSITION_ASSESSMENT, +} from "./codemod-v1-to-v2.ts"; +import type { PathwayMetadataV1 } from "../src/types/pathwayMetadata.v1.d.ts"; + +const V2_ID = "http://pathways.rmi.org/schema/pathwayMetadata.v2.json"; + +const WELL_FORMED = [ + "#### Pathway Description", + "", + "A description of the pathway.", + "", + "#### Core Drivers", + "", + "*Policy:* Policies drive it.", + "", + "#### Application to Transition Assessment", + "", + "How to apply it.", +].join("\n"); + +/** ACE-CNS-2024's shape: the Core Drivers heading lost its `####` markers. */ +const BARE_HEADING = [ + "#### Pathway Description", + "", + "A description of the pathway.", + "", + "Core Drivers", + "", + "*Policy:* Policies drive it.", + "", + "#### Application to Transition Assessment", + "", + "How to apply it.", +].join("\n"); + +describe("splitExpertOverview", () => { + it("splits the three ATX-headed sections", () => { + const s = splitExpertOverview(WELL_FORMED); + expect(s.get(PATHWAY_DESCRIPTION)).toBe("A description of the pathway."); + expect(s.get(CORE_DRIVERS)).toBe("*Policy:* Policies drive it."); + expect(s.get(TRANSITION_ASSESSMENT)).toBe("How to apply it."); + }); + + it("treats a bare section title as a heading (the ACE-CNS case)", () => { + const s = splitExpertOverview(BARE_HEADING); + // Without the tolerance, Core Drivers prose would land in the description + // and push it over the 2500-char limit. + expect(s.get(PATHWAY_DESCRIPTION)).toBe("A description of the pathway."); + expect(s.get(CORE_DRIVERS)).toBe("*Policy:* Policies drive it."); + }); + + it("does not treat a title mentioned mid-sentence as a heading", () => { + const s = splitExpertOverview( + [ + "#### Pathway Description", + "", + "The Core Drivers of this pathway are policy-led.", + ].join("\n"), + ); + expect(s.get(PATHWAY_DESCRIPTION)).toBe( + "The Core Drivers of this pathway are policy-led.", + ); + expect(s.has(CORE_DRIVERS)).toBe(false); + }); + + it("preserves markdown inside a section body", () => { + const s = splitExpertOverview( + ["#### Pathway Description", "", "para one.", "", "para two."].join("\n"), + ); + expect(s.get(PATHWAY_DESCRIPTION)).toBe("para one.\n\npara two."); + }); + + it("returns no sections for text with no recognised headings", () => { + expect(splitExpertOverview("Just prose.").size).toBe(0); + }); +}); + +function v1(over: Partial): PathwayMetadataV1 { + return { + $schema: "http://pathways.rmi.org/schema/pathwayMetadata.v1.json", + id: "X", + name: { full: "X" }, + description: "A pathway.", + publication: { + title: { full: "T" }, + publisher: { full: "TransitionZero" }, + year: 2024, + }, + pathwayType: "Normative", + geography: { global: true }, + sectors: [{ name: "Power", technologies: [] }], + expertOverview: WELL_FORMED, + metric: ["Capacity"], + keyFeatures: { + emissionsTrajectory: "Significant decrease", + energyEfficiency: "Moderate improvement", + energyDemand: "Minor increase", + electrification: "Significant increase", + policyTypes: ["Carbon price"], + technologyCostTrend: "Decrease", + emissionsScope: "CO2e (Kyoto)", + policyAmbition: "High ambition policies", + technologyCostsDetail: "Total costs", + newTechnologiesIncluded: ["CCUS"], + investmentNeeds: "By sector", + }, + ...over, + } as unknown as PathwayMetadataV1; +} + +describe("widestScope", () => { + it("uses the lone sector when there is only one", () => { + expect(widestScope(v1({})).sector).toBe("Power"); + }); + + it("uses cross-sector for a multi-sector pathway", () => { + const doc = v1({ + sectors: [ + { name: "Power", technologies: [] }, + { name: "Steel", technologies: [] }, + ], + } as Partial); + expect(widestScope(doc).sector).toBe("cross-sector"); + }); + + it("collapses a repeated sector name rather than calling it multi-sector", () => { + // pathwayMetadata has no uniqueItems on sectors, and the _full fixture + // deliberately lists Automotive twice. + const doc = v1({ + sectors: [ + { name: "Power", technologies: [] }, + { name: "Power", technologies: ["Solar"] }, + ], + } as Partial); + expect(widestScope(doc).sector).toBe("Power"); + }); + + it("prefers Global when the pathway is global, even with regions present", () => { + const doc = v1({ + geography: { global: true, regions: { "South East Asia": ["TH"] } }, + } as Partial); + expect(widestScope(doc).geography).toBe("Global"); + }); + + it("uses the lone region label (the ACE case)", () => { + const doc = v1({ + geography: { regions: { "South East Asia": ["TH", "VN"] } }, + } as Partial); + expect(widestScope(doc).geography).toBe("South East Asia"); + }); + + it("uses the lone country code", () => { + const doc = v1({ + geography: { country: ["TH"] }, + } as Partial); + expect(widestScope(doc).geography).toBe("TH"); + }); + + it("throws rather than guessing between several regions", () => { + const doc = v1({ + geography: { regions: { A: ["TH"], B: ["VN"] } }, + } as Partial); + expect(() => widestScope(doc)).toThrow(/ambiguous/); + }); + + it("throws rather than guessing between several countries", () => { + const doc = v1({ + geography: { country: ["TH", "VN"] }, + } as Partial); + expect(() => widestScope(doc)).toThrow(/ambiguous/); + }); + + it("throws when there is no geography at all", () => { + const doc = v1({ geography: {} } as Partial); + expect(() => widestScope(doc)).toThrow(/no geography/); + }); +}); + +describe("upgradeV1ToV2", () => { + it("repoints $schema at v2", () => { + expect(upgradeV1ToV2(v1({})).doc.$schema).toBe(V2_ID); + }); + + it("wraps every keyFeature as one entry at the widest scope", () => { + const { doc } = upgradeV1ToV2(v1({})); + const kf = doc.keyFeatures; + expect(Object.keys(kf)).toHaveLength(11); + for (const entries of Object.values(kf)) { + expect(entries).toHaveLength(1); + expect(entries[0].sector).toBe("Power"); + expect(entries[0].geography).toBe("Global"); + } + expect(kf.emissionsTrajectory[0].value).toBe("Significant decrease"); + }); + + it("nests an array-valued field rather than splatting it into entries", () => { + const doc = upgradeV1ToV2( + v1({ + keyFeatures: { + ...v1({}).keyFeatures, + policyTypes: ["Carbon price", "Subsidies"], + }, + } as Partial), + ).doc; + expect(doc.keyFeatures.policyTypes).toHaveLength(1); + expect(doc.keyFeatures.policyTypes[0].value).toEqual([ + "Carbon price", + "Subsidies", + ]); + }); + + it("moves the description section into pathwayDescription", () => { + const { doc } = upgradeV1ToV2(v1({})); + expect(doc.pathwayDescription).toBe("A description of the pathway."); + // The assessment section has no v2 field; it comes back in the result so the + // run report can print it rather than losing it. + expect("transitionAssessment" in doc).toBe(false); + }); + + it("discards pathwayOverview rather than folding it in", () => { + // #858 says to fold it in; the data owner confirmed on PR #898 that it should + // be dropped, because the two texts restate each other and the merged field + // reads as immediate self-repetition. + const { doc, droppedPathwayOverview } = upgradeV1ToV2( + v1({ pathwayOverview: "A short summary." } as Partial), + ); + expect(droppedPathwayOverview).toBe("A short summary.".length); + expect(doc.pathwayDescription).toBe("A description of the pathway."); + expect(doc.pathwayDescription).not.toContain("A short summary."); + }); + + it("reports nothing dropped when there was no pathwayOverview", () => { + expect(upgradeV1ToV2(v1({})).droppedPathwayOverview).toBe(0); + }); + + it("drops the v1 overview fields", () => { + const { doc } = upgradeV1ToV2( + v1({ pathwayOverview: "A short summary." } as Partial), + ); + expect("expertOverview" in doc).toBe(false); + expect("pathwayOverview" in doc).toBe(false); + }); + + it("scaffolds coreDrivers all-null and dependencies empty", () => { + const { doc } = upgradeV1ToV2(v1({})); + expect(Object.values(doc.coreDrivers)).toEqual([ + null, + null, + null, + null, + null, + null, + null, + ]); + expect(doc.dependencies).toEqual([]); + }); + + it("returns the retired Application-to-Transition-Assessment prose", () => { + expect(upgradeV1ToV2(v1({})).transitionAssessmentProse).toBe( + "How to apply it.", + ); + }); + + it("returns the Core Drivers prose it does not carry over", () => { + expect(upgradeV1ToV2(v1({})).coreDriversProse).toBe( + "*Policy:* Policies drive it.", + ); + }); + + it("nulls pathwayDescription when there is no description section", () => { + const { doc } = upgradeV1ToV2( + v1({ expertOverview: "No headings here." } as Partial), + ); + expect(doc.pathwayDescription).toBeNull(); + }); + + it("preserves unrelated fields and their order", () => { + const { doc } = upgradeV1ToV2( + v1({ modelYearNetzero: 2050 } as Partial), + ); + expect(doc.modelYearNetzero).toBe(2050); + // pathwayDescription takes expertOverview's slot; coreDrivers/dependencies + // follow keyFeatures. Key order keeps the data-file diffs readable. + const keys = Object.keys(doc); + expect(keys.indexOf("pathwayDescription")).toBeLessThan( + keys.indexOf("metric"), + ); + expect(keys.indexOf("coreDrivers")).toBeGreaterThan( + keys.indexOf("keyFeatures"), + ); + }); +}); + +describe("upgradeV1ToV2 — #461 technology reporting", () => { + it("reports nothing when every technology belongs to its sector", () => { + const { unscopedTechnologies } = upgradeV1ToV2( + v1({ + sectors: [{ name: "Power", technologies: ["Solar", "Wind"] }], + } as unknown as Partial), + ); + expect(unscopedTechnologies).toEqual([]); + }); + + it("reports a technology outside its sector's list", () => { + const { unscopedTechnologies } = upgradeV1ToV2( + v1({ + sectors: [{ name: "Power", technologies: ["Solar", "Hydrogen Use"] }], + } as unknown as Partial), + ); + expect(unscopedTechnologies).toEqual(["Power: Hydrogen Use"]); + }); + + it("reports technologies on a sector with no defined list", () => { + const { unscopedTechnologies } = upgradeV1ToV2( + v1({ + sectors: [{ name: "Steel", technologies: ["Hydrogen Use"] }], + } as unknown as Partial), + ); + expect(unscopedTechnologies).toEqual(["Steel: Hydrogen Use"]); + }); + + it("leaves the offending technologies in the output document", () => { + // Reporting, not fixing: deleting a technology someone recorded on purpose + // would be worse than a failing schema:check with a line in the report. + const { doc, unscopedTechnologies } = upgradeV1ToV2( + v1({ + sectors: [{ name: "Steel", technologies: ["Hydrogen Use"] }], + } as unknown as Partial), + ); + expect(unscopedTechnologies).toHaveLength(1); + expect(doc.sectors).toEqual([ + { name: "Steel", technologies: ["Hydrogen Use"] }, + ]); + }); + + it("is empty for the corpus shape — TransitionZero declares no technologies", () => { + expect(upgradeV1ToV2(v1({})).unscopedTechnologies).toEqual([]); + }); +}); diff --git a/scripts/codemod-v1-to-v2.ts b/scripts/codemod-v1-to-v2.ts new file mode 100644 index 00000000..26be1fc4 --- /dev/null +++ b/scripts/codemod-v1-to-v2.ts @@ -0,0 +1,393 @@ +/** + * One-shot codemod: pathwayMetadata v1 -> v2 (#858). + * + * Run over an explicit list of files, in place: + * + * npx ts-node --esm scripts/codemod-v1-to-v2.ts src/data/iea/IEA-NZE-2024.json ... + * npx ts-node --esm scripts/codemod-v1-to-v2.ts --dry-run src/data/iea + * + * A directory argument expands to the metadata files under it. Files already on v2 + * are skipped, so re-running is safe and the remaining 49 files are a re-run + * rather than a rewrite. + * + * This is a development tool, not a runtime path: the app does not convert v1 + * documents on load. Files that still carry the v1 $schema simply are not loaded + * once the loader points at v2. + * + * Two v1 prose fields are retired without replacement (confirmed with the data + * owner on PR #898): `pathwayOverview`, because it restates the description + * almost verbatim; and the '#### Application to Transition Assessment' section, + * because the new UI does not display it. Neither has a v2 home, so the section + * text is printed in the run report alongside the Core Drivers prose rather than + * being dropped silently. + * + * It also reports, without touching, technologies attached to a sector that does + * not list them (#461). Auto-fixing would mean either deleting a technology + * someone deliberately recorded or inventing a sector's taxonomy -- both worse + * than a line in the report, and `npm run schema:check` refuses the file anyway. + * + * What it does NOT do: populate `coreDrivers`. The v1 "#### Core Drivers" prose + * cannot be mapped onto the 7 named fields mechanically -- several paragraphs + * exceed the 500-char cap, the italic labels do not correspond 1:1 to the field + * names, and each section has unlabeled paragraphs with no destination. Per #858 + * the fields are scaffolded null for hand-authoring; the prose is printed in the + * report so it is not lost track of. + */ +import { promises as fs } from "node:fs"; +import { join, extname } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { PathwayMetadataV1 } from "../src/types/pathwayMetadata.v1.d.ts"; +import type { PathwayMetadataV2 } from "../src/types/pathwayMetadata.v2.d.ts"; +import { technologyBelongsToSector } from "../src/utils/timeseriesTaxonomy.ts"; + +const V1_ID = "http://pathways.rmi.org/schema/pathwayMetadata.v1.json"; +const V2_ID = "http://pathways.rmi.org/schema/pathwayMetadata.v2.json"; + +/** Widest sentinels, mirroring src/utils/validateScopes.ts. */ +const CROSS_SECTOR = "cross-sector"; +const GLOBAL_SCOPE = "Global"; + +/** + * The three sections every v1 `expertOverview` is built from. Verified against all + * 56 files: 55 have all three, and ACE-CNS-2024 is missing "Core Drivers" only + * because its heading lost its `####` markers (see splitExpertOverview). + */ +export const PATHWAY_DESCRIPTION = "Pathway Description"; +export const CORE_DRIVERS = "Core Drivers"; +export const TRANSITION_ASSESSMENT = "Application to Transition Assessment"; +export const SECTION_TITLES = [ + PATHWAY_DESCRIPTION, + CORE_DRIVERS, + TRANSITION_ASSESSMENT, +] as const; + +const CORE_DRIVER_FIELDS = [ + "policies", + "emissionsTargets", + "technologyCosts", + "investmentChange", + "macroeconomicDrivers", + "behavioralShifts", + "otherDrivers", +] as const; + +/** + * Split a v1 `expertOverview` into its named sections. + * + * Headings are ATX (`#### Core Drivers`), but a bare line whose entire content is + * a known section title also counts. That tolerance exists for exactly one file: + * ACE-CNS-2024.json lost the `####` on its "Core Drivers" heading, which would + * otherwise fold 1.5 KB of core-drivers prose into pathwayDescription and push it + * past the 2500-char limit. Treating the bare title as a heading is safe because + * the strings are long and specific enough not to occur as body text. + */ +export function splitExpertOverview(text: string): Map { + const sections = new Map(); + const lines = text.split("\n"); + let current: string | null = null; + let buffer: string[] = []; + + const flush = () => { + if (current !== null) sections.set(current, buffer.join("\n").trim()); + buffer = []; + }; + + for (const line of lines) { + const stripped = line.trim(); + const atx = /^#{1,6}\s*(.+?)\s*$/.exec(stripped); + const heading = atx ? atx[1] : stripped; + const known = SECTION_TITLES.find((t) => t === heading); + if (known && (atx || stripped === known)) { + flush(); + current = known; + continue; + } + if (current !== null) buffer.push(line); + } + flush(); + return sections; +} + +/** + * The widest scope an entry on this pathway can carry, per #858: `cross-sector` + * for a multi-sector pathway else its lone sector, and `Global` else the + * pathway's single declared region or country. + * + * Throws rather than guessing when a pathway declares several regions or several + * standalone countries without `global`, since which of them is "widest" is an + * authoring decision (`cross-region` exists for that case). No file in the corpus + * hits this today -- all 56 resolve. + */ +export function widestScope(doc: PathwayMetadataV1): { + sector: string; + geography: string; +} { + const names = [...new Set((doc.sectors ?? []).map((s) => s.name))]; + if (names.length === 0) throw new Error("pathway declares no sectors"); + const sector = names.length > 1 ? CROSS_SECTOR : names[0]; + + const geo = doc.geography ?? {}; + const regions = Object.keys(geo.regions ?? {}); + const countries = geo.country ?? []; + + let geography: string; + if (geo.global === true) { + geography = GLOBAL_SCOPE; + } else if (regions.length === 1) { + geography = regions[0]; + } else if (regions.length > 1) { + throw new Error( + `${regions.length} regions and no "global" flag: widest geography is ambiguous ` + + `(consider "cross-region"): ${regions.join(", ")}`, + ); + } else if (countries.length === 1) { + geography = countries[0]; + } else if (countries.length > 1) { + throw new Error( + `${countries.length} countries and no "global" flag or region: widest geography is ambiguous`, + ); + } else { + throw new Error("pathway declares no geography"); + } + + return { sector, geography }; +} + +/** Wrap one v1 value as a single scoped entry at the given scope. */ +function scoped(sector: string, geography: string, value: T) { + return [{ sector, geography, value }]; +} + +export interface UpgradeResult { + doc: PathwayMetadataV2; + /** Prose with no destination in v2 -- reported so it is not lost track of. */ + coreDriversProse: string; + /** Ditto: the new UI does not show this, so v2 has no field for it. */ + transitionAssessmentProse: string; + scope: { sector: string; geography: string }; + /** Chars of `pathwayOverview` discarded, for the run report. */ + droppedPathwayOverview: number; + /** + * `"Sector: Technology"` for each technology its sector does not list (#461). + * Carried straight over from v1, so this reports pre-existing data rather than + * anything the codemod introduced. + */ + unscopedTechnologies: string[]; +} + +/** Transform one v1 document into its v2 equivalent. Pure; does no I/O. */ +export function upgradeV1ToV2(doc: PathwayMetadataV1): UpgradeResult { + const scope = widestScope(doc); + const { sector, geography } = scope; + const kf = doc.keyFeatures; + + const sections = splitExpertOverview(doc.expertOverview ?? ""); + const description = sections.get(PATHWAY_DESCRIPTION) ?? ""; + const assessment = sections.get(TRANSITION_ASSESSMENT) ?? ""; + const coreDriversProse = sections.get(CORE_DRIVERS) ?? ""; + + // `pathwayOverview` is DISCARDED, not folded into pathwayDescription. + // + // #858 says to move it in, and an earlier version of this script did. Reading + // the output showed why that is wrong: the two texts restate each other almost + // verbatim, so the merged field opens by saying the same thing twice. IEA-STEPS + // for instance had "STEPS provides a sense of the energy sector's direction of + // travel today, based on the latest market data, technology costs and in-depth + // analysis of the prevailing policy settings..." immediately followed by "STEPS + // projects the energy sector's current direction of travel, based on the latest + // market data, technology costs and in-depth analysis of the stated policies...". + // + // Confirmed with the data owner (Jacob, PR #898): the field was already out of + // use and should be removed without replacement. It has no readers in the app, + // so nothing observable is lost. + const droppedPathwayOverview = doc.pathwayOverview?.trim().length ?? 0; + + const out: Record = {}; + for (const [key, value] of Object.entries(doc)) { + switch (key) { + case "$schema": + out.$schema = V2_ID; + break; + case "pathwayOverview": + // Superseded; emitted below in expertOverview's position. + break; + case "expertOverview": + out.pathwayDescription = description.length > 0 ? description : null; + break; + case "keyFeatures": + out.keyFeatures = { + emissionsTrajectory: scoped( + sector, + geography, + kf.emissionsTrajectory, + ), + energyEfficiency: scoped(sector, geography, kf.energyEfficiency), + energyDemand: scoped(sector, geography, kf.energyDemand), + electrification: scoped(sector, geography, kf.electrification), + policyTypes: scoped(sector, geography, kf.policyTypes), + technologyCostTrend: scoped( + sector, + geography, + kf.technologyCostTrend, + ), + emissionsScope: scoped(sector, geography, kf.emissionsScope), + policyAmbition: scoped(sector, geography, kf.policyAmbition), + technologyCostsDetail: scoped( + sector, + geography, + kf.technologyCostsDetail, + ), + newTechnologiesIncluded: scoped( + sector, + geography, + kf.newTechnologiesIncluded, + ), + investmentNeeds: scoped(sector, geography, kf.investmentNeeds), + }; + // Scaffolded for hand-authoring; see the file header. + out.coreDrivers = Object.fromEntries( + CORE_DRIVER_FIELDS.map((f) => [f, null]), + ); + out.dependencies = []; + break; + default: + out[key] = value; + } + } + + const unscopedTechnologies = (doc.sectors ?? []).flatMap((s) => + (s.technologies ?? []) + .filter((t) => technologyBelongsToSector(t, s.name) !== "yes") + .map((t) => `${s.name}: ${t}`), + ); + + return { + doc: out as unknown as PathwayMetadataV2, + unscopedTechnologies, + coreDriversProse, + transitionAssessmentProse: assessment, + scope, + droppedPathwayOverview, + }; +} + +async function metadataFilesUnder(path: string): Promise { + const stat = await fs.stat(path); + if (!stat.isDirectory()) return [path]; + const dirents = await fs.readdir(path, { withFileTypes: true }); + const found: string[] = []; + for (const d of dirents) { + const full = join(path, d.name); + if (d.isDirectory()) found.push(...(await metadataFilesUnder(full))); + else if (d.isFile() && extname(d.name) === ".json") found.push(full); + } + return found.sort(); +} + +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const paths = args.filter((a) => !a.startsWith("--")); + + if (paths.length === 0) { + console.error( + "usage: codemod-v1-to-v2.ts [--dry-run] [...]\n" + + "Refusing to run with no explicit target.", + ); + process.exit(1); + } + + const files = (await Promise.all(paths.map(metadataFilesUnder))).flat(); + const report: string[] = []; + let migrated = 0; + let skipped = 0; + + for (const file of files) { + const raw = await fs.readFile(file, "utf8"); + const parsed = JSON.parse(raw) as { $schema?: string }; + if (parsed.$schema === V2_ID) { + skipped++; + continue; + } + if (parsed.$schema !== V1_ID) { + // Timeseries files and anything else non-metadata. + skipped++; + continue; + } + + const doc = parsed as unknown as PathwayMetadataV1; + let result: UpgradeResult; + try { + result = upgradeV1ToV2(doc); + } catch (e) { + console.error(`✖ ${file}: ${String(e instanceof Error ? e.message : e)}`); + process.exitCode = 1; + continue; + } + + const { scope, coreDriversProse, droppedPathwayOverview } = result; + const descLength = result.doc.pathwayDescription?.length ?? 0; + console.info( + `✔ ${file}\n` + + ` scope: (${scope.sector}, ${scope.geography})` + + ` pathwayDescription: ${descLength} chars` + + (droppedPathwayOverview > 0 + ? ` [dropped ${droppedPathwayOverview}-char pathwayOverview]` + : ""), + ); + if (result.unscopedTechnologies.length > 0) { + // stderr, because this needs a human before the file will pass + // schema:check -- but not a non-zero exit, since the migration itself + // succeeded and the offending data predates it. + console.error( + ` #461: technologies outside their sector's list, left as-is: ` + + result.unscopedTechnologies.join("; "), + ); + } + + const orphaned: string[] = []; + if (result.coreDriversProse.length > 0) { + orphaned.push(`### ${CORE_DRIVERS}\n\n${result.coreDriversProse}`); + } else { + console.info(` note: no "${CORE_DRIVERS}" section found`); + } + if (result.transitionAssessmentProse.length > 0) { + orphaned.push( + `### ${TRANSITION_ASSESSMENT}\n\n${result.transitionAssessmentProse}`, + ); + } + if (orphaned.length > 0) { + report.push(`## ${file}\n\n${orphaned.join("\n\n")}\n`); + } + + if (!dryRun) { + await fs.writeFile(file, `${JSON.stringify(result.doc, null, 2)}\n`); + } + migrated++; + } + + console.info( + `\n${dryRun ? "Would migrate" : "Migrated"} ${migrated} file(s); skipped ${skipped}.`, + ); + + if (report.length > 0) { + console.info( + "\n--- v1 prose with no v2 destination ---\n" + + `"${CORE_DRIVERS}" becomes the structured coreDrivers object, scaffolded null here;\n` + + `"${TRANSITION_ASSESSMENT}" is retired because the new UI does not display it.\n` + + "Both are reproduced below so the text is not lost.\n", + ); + console.info(report.join("\n")); + } + if (!dryRun) { + console.info("Run `npm run schema:check` and `npx prettier --write` next."); + } +} + +// Only run when invoked directly, so the exported helpers stay importable by tests. +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + main().catch((e: unknown) => { + console.error(String(e instanceof Error ? e.stack : e)); + process.exit(1); + }); +} diff --git a/scripts/schema-check-files.ts b/scripts/schema-check-files.ts index d079d859..76c38d55 100644 --- a/scripts/schema-check-files.ts +++ b/scripts/schema-check-files.ts @@ -5,8 +5,14 @@ import type { FileEntry } from "../src/utils/validateData.ts"; import { validateFilesBySchema } from "../src/utils/validateData.ts"; import { decideIncludeInvalid } from "../src/utils/loadData.ts"; import pathwayMetadata from "../src/schema/pathwayMetadata.v1.json" with { type: "json" }; +import pathwayMetadataV2 from "../src/schema/pathwayMetadata.v2.json" with { type: "json" }; import pathwayTimeseries from "../src/schema/pathwayTimeseries.v1.json" with { type: "json" }; import { commonSchemas } from "../src/schema/common/index.ts"; +import type { PathwayMetadataV2 } from "../src/types/pathwayMetadata.v2.d.ts"; +import { + PATHWAY_METADATA_V2_ID, + validateScopedEntries, +} from "../src/utils/validateScopes.ts"; async function run(dir: string) { async function getJsonFilesRecursive(base: string): Promise { @@ -34,10 +40,29 @@ async function run(dir: string) { const { valid, invalid } = validateFilesBySchema(entries, [ pathwayMetadata, + pathwayMetadataV2, pathwayTimeseries, ...commonSchemas, ]); - return { dir, validCount: valid.length, invalid }; + + // Second pass over the AJV-valid v2 documents for the cross-field scope + // constraint draft-07 cannot express — see src/utils/validateScopes.ts. A + // document that fails here is reported exactly like a schema failure, so a + // mistyped region label breaks the build instead of silently matching nothing. + const scopeProblems = valid + .filter((r) => r.schemaId === PATHWAY_METADATA_V2_ID) + .map((r) => ({ + name: r.name, + errors: validateScopedEntries(r.data as PathwayMetadataV2), + })) + .filter((p) => p.errors.length > 0); + + const badNames = new Set(scopeProblems.map((p) => p.name)); + return { + dir, + validCount: valid.filter((r) => !badNames.has(r.name)).length, + invalid: [...invalid, ...scopeProblems], + }; } async function main() { @@ -73,8 +98,12 @@ async function main() { filesWithIssues += r.invalid.length; for (const p of r.invalid) { - // Emit up to N errors per file as GH annotations - const file = join(r.dir, p.name); + // Emit up to N errors per file as GH annotations. + // `p.name` already carries the directory — getJsonFilesRecursive builds it + // with join(base, d.name) — so re-prepending r.dir produced paths like + // "src/data/src/data/foo.json" and the annotations pointed nowhere. Latent + // until now because it only shows when a file actually fails. + const file = p.name; const errs = p.errors.slice(0, 50); for (const e of errs) { console.log(`::${annotate} file=${file}::${e}`); diff --git a/src/components/KeyFeatures.test.tsx b/src/components/KeyFeatures.test.tsx index d452faf1..b4311262 100644 --- a/src/components/KeyFeatures.test.tsx +++ b/src/components/KeyFeatures.test.tsx @@ -3,19 +3,28 @@ import { render, screen } from "@testing-library/react"; import KeyFeatures from "./KeyFeatures"; import type { PathwayMetadataType } from "../types"; +/** + * v2 scopes every keyFeature as {sector, geography, value} entries (#858). These + * fixtures use a single widest-scope entry per field — what the codemod produces — + * so the rendering assertions below still describe v1's output. + */ +const wide = (value: T) => [ + { sector: "cross-sector", geography: "Global", value }, +]; + const mockKeyFeatures: PathwayMetadataType["keyFeatures"] = { - emissionsScope: "CO2", - emissionsTrajectory: "Moderate decrease", - energyEfficiency: "Minor improvement", - energyDemand: "Low or no change", - electrification: "Moderate increase", - policyTypes: ["Carbon price", "Subsidies"], - policyAmbition: "NDCs incl. conditional targets", - newTechnologiesIncluded: ["CCUS", "Battery storage"], - technologyCostTrend: "Decrease", - technologyCostsDetail: "Total costs", - investmentNeeds: "By technology", -}; + emissionsScope: wide("CO2"), + emissionsTrajectory: wide("Moderate decrease"), + energyEfficiency: wide("Minor improvement"), + energyDemand: wide("Low or no change"), + electrification: wide("Moderate increase"), + policyTypes: wide(["Carbon price", "Subsidies"]), + policyAmbition: wide("NDCs incl. conditional targets"), + newTechnologiesIncluded: wide(["CCUS", "Battery storage"]), + technologyCostTrend: wide("Decrease"), + technologyCostsDetail: wide("Total costs"), + investmentNeeds: wide("By technology"), +} as unknown as PathwayMetadataType["keyFeatures"]; describe("KeyFeatures", () => { it("renders all four group headers", () => { @@ -86,7 +95,7 @@ describe("KeyFeatures", () => { it("sentiment feature: unfavorable value uses red scale color", () => { const unfavorable = { ...mockKeyFeatures, - emissionsTrajectory: "Significant increase", + emissionsTrajectory: wide("Significant increase"), } as unknown as PathwayMetadataType["keyFeatures"]; render(); @@ -101,7 +110,9 @@ describe("KeyFeatures", () => { it("sentiment feature: no-info value renders a Badge, not a colored text span", () => { const noInfo = { ...mockKeyFeatures, - emissionsTrajectory: undefined, + // v2's way of saying "nothing authored at any scope" is an empty entry + // array, not a missing field — the field stays required. + emissionsTrajectory: [], } as unknown as PathwayMetadataType["keyFeatures"]; render(); diff --git a/src/components/KeyFeatures.tsx b/src/components/KeyFeatures.tsx index d68ce2a9..ab65907b 100644 --- a/src/components/KeyFeatures.tsx +++ b/src/components/KeyFeatures.tsx @@ -1,6 +1,7 @@ import React from "react"; import { PathwayMetadataType } from "../types"; import { getKeyFeatureTooltip } from "../utils/tooltipUtils"; +import { widestValue } from "../utils/keyFeatureScope"; import TextWithTooltip from "./TextWithTooltip"; import Badge from "./Badge"; import SentimentScale, { getSentimentPalette } from "./SentimentScale"; @@ -238,7 +239,11 @@ export const FeatureItem: React.FC = ({ labelClassName = "text-xs font-medium text-rmigray-500", showLabel = true, }) => { - const rawValue = keyFeatures[feature.key]; + // v2 stores each feature as scoped {sector, geography, value} entries (#858). + // Render the value at the broadest scope, which reproduces v1's output exactly + // for codemod-migrated data (one entry, at its widest scope). #869 replaces this + // with a scope-aware resolver and #859 adds the badge that names the scope. + const rawValue = widestValue(keyFeatures[feature.key]); const label =

{feature.label}

; diff --git a/src/data/README.md b/src/data/README.md index 91bac4ac..ce6fa308 100644 --- a/src/data/README.md +++ b/src/data/README.md @@ -1,110 +1,217 @@ # Pathway metadata for the tpr repo The `src/data` directory in the [tpr](https://github.com/RMI/tpr) repo contains all of the data shown on the Transition Pathways Repository site. -Each JSON file in this directory contains one or more pathway definitions. +Each JSON file in this directory contains one pathway definition, alongside optional `*_timeseries.json` files holding that pathway's data series. ## Schema and Validation -The JSON files have a strict, specific format that needs to be followed, which is defined by the JSON schema file in this repo at [pbtar_schema.json](https://github.com/RMI/tpr/blob/main/src/schema/schema.json). +The JSON files have a strict, specific format, defined by the JSON schema files in [`src/schema/`](https://github.com/RMI/tpr/tree/main/src/schema). +The schema is split across several files: the pathway metadata schema itself, plus shared definitions under `src/schema/common/` that it references (country codes, sector and technology names, publication details, and so on). -The schema file defines a number of mandatory fields which must be included. -Additionally, the structure, the data types, and in some cases the allowed values for a given key must be correct for things to work as expected. -This repo has CI/CD setup to validate any new JSON added in a PR against the schema. -Therefore, any new JSON added through a PR on main must pass all of the tests before being merged. +The schema defines a number of mandatory fields which must be included. +The structure, the data types, and in many cases the allowed values for a given key must all be correct for things to work as expected. +This repo has CI/CD set up to validate any new JSON added in a PR, so any new JSON added through a PR on `main` must pass before being merged. -After preparing a JSON file, you can run `npm run json:check`, which will trigger validation (locally) against all JSON files in this directory. -You can preview a JSON file as it will appear in the UI with `npm run dev`. -To add a new Pathway, a new JSON file in the correct format needs to be added to this repo in a pull request on `main`. +After preparing a JSON file, validate it locally with: -## Examples +```bash +npm run schema:check +``` -To see example files, look in this directory, or `testdata/valid/`. +That checks every file under `src/data` and `testdata/valid`. You can preview a file as it will appear in the UI with `npm run dev`. -## Creating new pathway files +If you have changed a schema rather than a data file, run `npm run schema` instead — it validates and then regenerates the TypeScript types in `src/types/`. Those are checked in, and CI fails if they are out of date. -To facilitate creating a new JSON file in the appropriate format using R, we have created the following two functions to validate a `` object against the schema in this repo, and to write a valid nested `` to a JSON file. -These functions can be copy-pasted to your R console and then they're available to use on any `` you have in your environment. -These functions require the following R packages to be installed in your environment: `jsonvalidate`, `jsonlite`, `dplyr`, `tidyr`, `stringr`, and `purrr`. +### Two schema versions -```r -# if schema_url is not provided, it will validate against the current PROD schema. +Two versions of the metadata schema currently exist: -validate_json <- function(json_obj, schema_url = NULL) { - if (is.null(schema_url)) { - schema_url <- "https://raw.githubusercontent.com/RMI/tpr/refs/heads/main/pbtar_schema.json" - } - json_schema <- readr::read_file(file = schema_url) - validation <- - jsonvalidate::json_validate( - json = jsonlite::toJSON(json_obj, auto_unbox = TRUE), - schema = json_schema, - verbose = TRUE, - greedy = TRUE, - engine = "ajv" - ) - if (!validation) { - errors <- - attr(validation, "errors") |> - dplyr::mutate(key = stringr::str_extract(instancePath, "[a-z]+")) |> - tidyr::unnest(params) |> - dplyr::rename(input = data) |> - dplyr::select(dplyr::any_of(c("input", "key", "message", "allowedValues"))) - return(errors) - } +- `pathwayMetadata.v2.json` — the current format. **New pathways should use this.** +- `pathwayMetadata.v1.json` — the previous format, still present so existing files remain valid. + +Every file declares which one it follows via its own `$schema` key, and the validator routes each file to the matching schema. A file on either version will pass `npm run schema:check`. + +**Only v2 files are loaded by the site.** A file still on v1 validates but does not appear in the app; the dev server logs how many were skipped. Migration of the remaining v1 files is in progress. + +## The v2 format + +A complete, valid example lives at [`testdata/valid/pathwayMetadata_v2_full.json`](../../testdata/valid/pathwayMetadata_v2_full.json), and a minimal one at `pathwayMetadata_v2_minimal.json`. Real pathways are in the publisher subdirectories here. + +If you are used to the v1 format, these are the differences that matter: + +### keyFeatures are scoped + +In v1 each of the 11 key features held a single value for the whole pathway. In v2 each holds an **array of entries**, so a pathway can record different values for different parts of its coverage: + +```json +"keyFeatures": { + "emissionsTrajectory": [ + { "sector": "cross-sector", "geography": "Global", "value": "Moderate decrease" }, + { "sector": "Power", "geography": "South East Asia", "value": "Significant decrease" } + ] } +``` -write_json <- function(json_obj, file) { - validation <- validate_json(json_obj) - if (!is.data.frame(validation)) { - jsonlite::write_json( - x = json_obj, - path = file, - auto_unbox = TRUE, - pretty = TRUE - ) - return(invisible()) +- `sector` is one of the sector names, or `"cross-sector"` meaning "all of the sectors this pathway covers". +- `geography` is `"Global"`, one of the region labels used in this pathway's own `geography.regions`, or one of its country codes. +- `value` is exactly what v1 held for that field — the same allowed values. For the two fields that were arrays in v1 (`policyTypes`, `newTechnologiesIncluded`), `value` is still an array. + +Both `sector` and `geography` must be something the pathway actually declares, or one of the widest sentinels. A region label that does not appear in the pathway's own `geography.regions` is rejected, which is what catches a typo like `"Southeast Asia"` where the pathway says `"South East Asia"`. + +If a feature does not vary, give it **one entry at the widest scope that applies** — `cross-sector` for a multi-sector pathway (otherwise its only sector), and `Global` for a global pathway (otherwise its region or country). + +An empty array means nothing is recorded at any scope. That is different from an entry whose `value` is `"No information"`, which is a deliberate statement that this scope has no data. + +### expertOverview is replaced by pathwayDescription + +v1's single `expertOverview` was one markdown document containing three sections. Only +one survives as prose in v2: + +- `pathwayDescription` (required, may be `null`, max 2500 chars) — the narrative description. +- the "Core Drivers" section becomes the structured `coreDrivers` object below. +- the "Application to Transition Assessment" section is **retired** — the new UI + does not display it, so v2 has no field for it. + +v1's separate `pathwayOverview` field is **retired without replacement**. Do not +carry it into `pathwayDescription`: in practice the two texts restate each other, +so merging them reads as immediate self-repetition. The field had no readers in +the app, so nothing is lost by dropping it. + +### coreDrivers and dependencies are new + +`coreDrivers` is an object with seven fields, all required but each allowed to be `null`. `null` means "not a core driver for this pathway", which is deliberately different from a driver that applies but has not been described. + +```json +"coreDrivers": { + "policies": "Carbon pricing across both covered sectors.", + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null +} +``` + +`dependencies` is an array — use `[]` if there are none. Each entry names a category, describes the dependency, scopes it to one of the pathway's sectors, and states how strong the evidence is: + +```json +"dependencies": [ + { + "dependency_name": "Infrastructure and logistics", + "dependency_description": "Grid buildout must keep pace with renewable additions.", + "sector": "Power", + "evidence_type": "Quantitative" } - validation +] +``` + +The allowed values for `dependency_name` and `evidence_type` are listed in the schema. + +## Migrating an existing v1 file + +There is a script for this — don't do it by hand: + +```bash +npx ts-node --esm scripts/codemod-v1-to-v2.ts --dry-run src/data/ +``` + +Drop `--dry-run` to write the changes. It rewrites files in place, skips anything already on v2, and prints the scope it chose for each file. + +It does **not** fill in `coreDrivers` — the v1 "Core Drivers" prose does not map onto the seven named fields mechanically, so the script scaffolds them all to `null` and prints the original text for whoever authors them. Run `npx prettier --write` on the files afterwards, then `npm run schema:check`. + +## Creating new pathway files + +To create a new file in the appropriate format using R, the function below writes a nested `` out as JSON. +It can be copy-pasted to your R console and requires the `jsonlite` package. + +```r +write_json <- function(json_obj, file) { + jsonlite::write_json( + x = json_obj, + path = file, + auto_unbox = TRUE, + pretty = TRUE, + null = "null" + ) } ``` -Once the above functions have been loaded in your R environment, a new `` can be created, and then validated and exported as a JSON file using these functions like so... +Note on validating from R: the schema is split across several files that reference each other by URL, and the usual `jsonvalidate::json_validate()` call takes a single schema and will not fetch those references. Write the file first, then validate it with `npm run schema:check`, which resolves them correctly and also runs the checks that JSON Schema alone cannot express — such as confirming each `sector` and `geography` is one the pathway declares. + +Once the function above is loaded, a new pathway can be created and written out like so. ```r -# Note that single-element arrays must be wrapped with I(), the identity function, to ensure that `jsonlite` processes them as arrays, rather than length-1 vectors (everything is a vector in R). +# Single-element vectors must be wrapped with I(), the identity function, so that +# `jsonlite` writes them as arrays rather than as bare values (everything is a +# vector in R). Fields that must be `null` rather than absent use NA... see below. + +scoped <- function(value) { + list(list(sector = "Power", geography = "VN", value = value)) +} new_pathway_metadata <- list( - list( - id = "R-import-example", - name = "R Import Pathway", - description = "Pathway Imported from R", - pathwayType = "Normative", - modelYearEnd = 2050, - modelTempIncrease = 1.5, - geography = list("Global", "US", "Europe"), - sectors = list( - list(name = "Power", technologies = c("Coal", "Wind")), - list(name = "Steel", technologies = I(c("Other"))) - ), - publisher = "Example Publisher", - publicationYear = 2021, - metric = I(c("Capacity")), # `I()` is necessary so that jsonlite parses it as a length 1 array - expertOverview = "Text based expert recommendation long.", - dataSource = list( - description = "Data source description.", - url = "https://www.example.com/", - downloadAvailable = FALSE + `$schema` = "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", + id = "R-import-example", + name = list(full = "R Import Pathway", short = "R Example"), + description = "Pathway imported from R.", # must end in a period + publication = list( + title = list(full = "Example Publication"), + publisher = list(full = "TransitionZero"), + year = 2021, + links = list( + list(description = "Report", url = "https://www.example.com/") + ) + ), + pathwayType = "Normative", + modelYearEnd = 2050, + modelTempIncrease = 1.5, + geography = list( + regions = list(`South East Asia` = I(c("VN", "TH"))), + country = I(c("VN")) + ), + sectors = list( + list(name = "Power", technologies = I(c("Coal", "Wind"))) + ), + pathwayDescription = "A short narrative description of the pathway.", + metric = I(c("Capacity")), + keyFeatures = list( + emissionsTrajectory = scoped("Moderate decrease"), + energyEfficiency = scoped("Moderate improvement"), + energyDemand = scoped("Minor increase"), + electrification = scoped("Significant increase"), + policyTypes = scoped(I(c("Carbon price"))), + technologyCostTrend = scoped("Decrease"), + emissionsScope = scoped("CO2"), + policyAmbition = scoped("Current/legislated policies"), + technologyCostsDetail = scoped("Total costs"), + newTechnologiesIncluded = scoped(I(c("Battery storage"))), + investmentNeeds = scoped("By sector") + ), + coreDrivers = list( + policies = NULL, emissionsTargets = NULL, technologyCosts = NULL, + investmentChange = NULL, macroeconomicDrivers = NULL, + behavioralShifts = NULL, otherDrivers = NULL + ), + dependencies = list( + list( + dependency_name = "Technology", + dependency_description = "Grid capacity must expand.", + sector = "Power", + evidence_type = "Qualitative" ) ) ) -validate_json(new_pathway_metadata) - -write_json(new_pathway_metadata, "test.json") +write_json(new_pathway_metadata, "src/data/example-publisher/EXAMPLE-2021.json") ``` -If the `` is not valid, the functions will return a data frame with information about what was invalid. +One R-specific note: `jsonlite` writes an R `NULL` as `{}` by default, which the schema rejects. That is why `write_json` above passes `null = "null"` — with it, the seven `NULL` entries in `coreDrivers` come out as JSON `null` as intended. All seven keys are required even when every one of them is null, so keep them all. + +The example above has been run as written, and the file it produces passes `npm run schema:check`. + +## Keeping this up to date This README should be the definitive source of information about these JSON files and how to add them or modify them. As this repo is currently under heavy development, such details may change rapidly, and this README should be kept up to date with those changes as they happen. diff --git a/src/data/asean-centre-for-energy/ACE-ATS-2024.json b/src/data/asean-centre-for-energy/ACE-ATS-2024.json index ce43752f..819ba14e 100644 --- a/src/data/asean-centre-for-energy/ACE-ATS-2024.json +++ b/src/data/asean-centre-for-energy/ACE-ATS-2024.json @@ -1,5 +1,5 @@ { - "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v1.json", + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", "id": "ACE-ATS-2024", "publication": { "title": { @@ -72,7 +72,7 @@ "technologies": [] } ], - "expertOverview": "#### Pathway Description\n\nThe ASEAN Centre for Energy (ACE) acts as the ASEAN energy data center and knowledge hub and produces the ACE ASEAN Energy Outlook providing energy pathways for Southeast Asia. The ACE ASEAN Member States Targets Scenario (ATS) is an exploratory pathway which assumes that every ASEAN member state fully achieves its unconditional transition targets on time, including unconditional goals set via Nationally Determined Contributions (NDCs). This pathway models the impact of existing policies, such as national power development plans (PDPs), and the implementation of these stated targets. The ATS pathway models more ambitious energy efficiency targets, as well as a faster deployment of low-carbon energy technologies, especially from renewable energy. The ATS pathway does not include more aspirational national objectives, such as conditional NDCs or long-term net zero goals. For examples of pathways that include these policies, see the ACE Regional Aspiration Scenario (RAS) and IEA Announced Pledges Scenario (APS).\n\nPower generation emissions decline -2.4% per year, resulting in a total decrease of 74% relative to power emissions in the Baseline Scenario. However, the ATS pathway projects to an average annual emissions increase of 0.8% across the full energy sector. \n\n#### Core Drivers\n\nThe ATS pathway is primarily influenced by existing and announced policies and does not consider declining technology costs or significant new technologies.\n\n*Policy:* Unconditional national policies are the primary driver of transition outcomes in the ATS pathway. Energy efficiency targets and renewable energy targets, including detailed plans from national power development plans, drive significant renewables deployment and emissions reductions. Power capacity deployment follows the plans detailed in NDCs and PDPs until the final year laid out in these policies, and ATS simulates further additions based on the technology mix at the end year of the policy plans.\n\nThe ATS pathway assumes technology costs remain fixed at current levels across the full trajectory. The ATS pathway provides information on demand shifts, investment flows, and technology deployment, but these factors are driven primarily by modeled policy shifts. \n\n#### Application to Transition Assessment\n\nACE ATS is an exploratory, policy-focused pathway, which provides useful values for assessing the alignment of corporate plans to region-wide policy impacts. As a region-specific pathway, ACE ATS is not directly linked to a global implied temperature rise, limiting its use as a quantitative benchmark the ambition of climate targets.\n\nThe ATS is particularly well-suited for evaluating the alignment of corporate targets, strategies, and investment pipelines with the stated ambitions of the jurisdictions in which they operate. Its strong policy orientation allows for a meaningful comparison between corporate actions and national development priorities. A misalignment with the ATS may signal potential exposure to future regulatory risks, such as non-compliance with evolving energy policies or reduced competitiveness in markets where low-carbon technologies are being prioritized. Alignment, in the other hand, may indicate that a company is strategically positioned to benefit from market shifts. The ATS pathway includes the impacts of stated policies and targets which have not yet been implemented; therefore, misalignment does not necessarily imply exposure to current regulatory risk but rather a potential gap in future readiness. However, the ATS pathway excludes aspirational goals such as conditional NDCs, making the pathway a relatively conservative choice for evaluating potential policy alignment and impact.\n\nDue to its assumption of static technology costs and limited modelling of specific technology deployment, the ATS pathway has more limited applications in assessing the commercial or technological feasibility of specific transition strategies. Users interested in these applications may consider supplementing the ATS pathway with additional pathways that provide more detailed and dynamic technology cost and deployment projections.\n\nATS provides national targets. These targets are presented with fine granularity, enabling detailed national policy alignment assessments. The pathway also includes regional-level projections for generation and capacity on 5-year intervals and using a moderately detailed breakdown by energy sources such as coal, wind, solar, biomass, and geothermal. This data is relevant and useful for analyzing specific decarbonization levers and project investment pipelines within company plans, and supports region-specific benchmark, but does not enable country-specific comparisons of changes in generation or capacity.", + "pathwayDescription": "The ASEAN Centre for Energy (ACE) acts as the ASEAN energy data center and knowledge hub and produces the ACE ASEAN Energy Outlook providing energy pathways for Southeast Asia. The ACE ASEAN Member States Targets Scenario (ATS) is an exploratory pathway which assumes that every ASEAN member state fully achieves its unconditional transition targets on time, including unconditional goals set via Nationally Determined Contributions (NDCs). This pathway models the impact of existing policies, such as national power development plans (PDPs), and the implementation of these stated targets. The ATS pathway models more ambitious energy efficiency targets, as well as a faster deployment of low-carbon energy technologies, especially from renewable energy. The ATS pathway does not include more aspirational national objectives, such as conditional NDCs or long-term net zero goals. For examples of pathways that include these policies, see the ACE Regional Aspiration Scenario (RAS) and IEA Announced Pledges Scenario (APS).\n\nPower generation emissions decline -2.4% per year, resulting in a total decrease of 74% relative to power emissions in the Baseline Scenario. However, the ATS pathway projects to an average annual emissions increase of 0.8% across the full energy sector.", "metric": [ "Emissions Intensity", "Capacity", @@ -81,22 +81,98 @@ "Absolute Emissions" ], "keyFeatures": { - "emissionsTrajectory": "Moderate increase", - "energyEfficiency": "Moderate improvement", - "energyDemand": "Significant increase", - "electrification": "Low or no change", + "emissionsTrajectory": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Moderate increase" + } + ], + "energyEfficiency": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Moderate improvement" + } + ], + "energyDemand": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Significant increase" + } + ], + "electrification": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Low or no change" + } + ], "policyTypes": [ - "Phaseout dates", - "Subsidies", - "Target technology shares", - "Performance standards", - "Other" + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": [ + "Phaseout dates", + "Subsidies", + "Target technology shares", + "Performance standards", + "Other" + ] + } + ], + "technologyCostTrend": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Low or no change" + } ], - "technologyCostTrend": "Low or no change", - "emissionsScope": "CO2e (unspecified GHGs)", - "policyAmbition": "NDCs, unconditional only", - "technologyCostsDetail": "Capital costs, O&M, etc.", - "newTechnologiesIncluded": ["Green H2/ammonia", "SAF"], - "investmentNeeds": "By sector" - } + "emissionsScope": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "CO2e (unspecified GHGs)" + } + ], + "policyAmbition": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "NDCs, unconditional only" + } + ], + "technologyCostsDetail": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Capital costs, O&M, etc." + } + ], + "newTechnologiesIncluded": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": ["Green H2/ammonia", "SAF"] + } + ], + "investmentNeeds": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "By sector" + } + ] + }, + "coreDrivers": { + "policies": null, + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [] } diff --git a/src/data/asean-centre-for-energy/ACE-BAS-2024.json b/src/data/asean-centre-for-energy/ACE-BAS-2024.json index 1547ce38..4c267a33 100644 --- a/src/data/asean-centre-for-energy/ACE-BAS-2024.json +++ b/src/data/asean-centre-for-energy/ACE-BAS-2024.json @@ -1,5 +1,5 @@ { - "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v1.json", + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", "id": "ACE-BAS-2024", "publication": { "title": { @@ -72,7 +72,7 @@ "technologies": [] } ], - "expertOverview": "#### Pathway Description\n\nThe ASEAN Centre for Energy (ACE) acts as the ASEAN energy data center and knowledge hub and produces the ACE ASEAN Energy Outlook providing energy pathways for Southeast Asia. The ACE Baseline scenario (BAS) is a predictive pathway that extrapolates observed historic trends of the ASEAN Member States (AMS) energy systems into the future. It assumes a business-as-usual level of effort and specifically disregards modelling any policy interventions, even existing ones to meet national energy efficiency (EE) and renewable energy (RE) targets. Hence, it also excludes firm plant capacity additions based on power development plans (PDP). The extrapolation of past trends also implies that technology costs remain static and new technologies do not benefit from learning rates.\n\nAs a conservative baseline pathway, ACE BAS forecasts emissions at the ASEAN level to more than double by 2050.\n\n#### Core Drivers\n\nThe BAS pathway is driven by historical trends in energy consumption growth.\n\n*Economic growth:* Rising energy consumption in the BAS pathway is a function of region-wide population growth and rapid economic development. The total population in ASEAN is forecast to grow from 680 million in 2022 to 790 million in 2050. GDP growth across the region is projected to be 3.9% CAGR between that same period, along with further increases in electrification rate and clean cooking access.\n\nPolicy impacts are explicitly excluded from this model in order to provide a baseline case for modeling policy impacts, which are included in other ACE pathways such as the Regional Aspiration Scenario (RAS). Energy efficiency and technology costs are assumed to be static, leaving limited avenues for potential emissions reductions in this baseline pathway.\n\n#### Application to Transition Assessment\n\nThe ACE BAS pathway is intended to provide a base case for the ASEAN region, explicitly excluding policy impacts, technology cost declines, or efficiency improvements as avenues for emissions reductions. As a result, it provides a highly conversative reference, which may be used to place a floor on potential future rates of transition in the energy sector. As a single-region model, the BAS pathway is not associated with a global implied temperature rise, and should not be used as a quantitative benchmark for the ambition of climate targets.\n\nMisalignment to the baseline scenario can highlight companies or plans which do not keep pace with purely historical trends, without accounting for planned policy interventions. However, the scenario’s conservative limit its applications in assessing the ambition of corporate targets.\n\nFor transition assessment applications focused on assessing the commercial or technological feasibility of company plans, BAS’s assumptions of static technology costs and energy efficiency may be overly conservative compared to alternative business-as-usual pathways. Due to its deliberate exclusion of policy impacts, including existing policies, the BAS pathway should not be used for assessing the alignment of company strategies with existing or potential policies, and ACE provides alternate pathways for these applications.\n\nThe BAS provides benchmark data on 5-year intervals and uses a moderately detailed breakdown of specific generation technologies such as coal, wind, solar, biomass, and geothermal. This level of detail makes it well-suited to assessing specific decarbonization levers and project pipelines within company plans. BAS provides data at the regional ASEAN level, which enables region-specific benchmarking but limits its applicability for assessing country-specific strategies.", + "pathwayDescription": "The ASEAN Centre for Energy (ACE) acts as the ASEAN energy data center and knowledge hub and produces the ACE ASEAN Energy Outlook providing energy pathways for Southeast Asia. The ACE Baseline scenario (BAS) is a predictive pathway that extrapolates observed historic trends of the ASEAN Member States (AMS) energy systems into the future. It assumes a business-as-usual level of effort and specifically disregards modelling any policy interventions, even existing ones to meet national energy efficiency (EE) and renewable energy (RE) targets. Hence, it also excludes firm plant capacity additions based on power development plans (PDP). The extrapolation of past trends also implies that technology costs remain static and new technologies do not benefit from learning rates.\n\nAs a conservative baseline pathway, ACE BAS forecasts emissions at the ASEAN level to more than double by 2050.", "metric": [ "Emissions Intensity", "Capacity", @@ -81,16 +81,92 @@ "Absolute Emissions" ], "keyFeatures": { - "emissionsTrajectory": "Significant increase", - "energyEfficiency": "Minor improvement", - "energyDemand": "Significant increase", - "electrification": "Low or no change", - "policyTypes": ["None"], - "technologyCostTrend": "Low or no change", - "emissionsScope": "CO2e (unspecified GHGs)", - "policyAmbition": "No policies included", - "technologyCostsDetail": "Capital costs, O&M, etc.", - "newTechnologiesIncluded": ["No new technologies"], - "investmentNeeds": "By sector" - } + "emissionsTrajectory": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Significant increase" + } + ], + "energyEfficiency": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Minor improvement" + } + ], + "energyDemand": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Significant increase" + } + ], + "electrification": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Low or no change" + } + ], + "policyTypes": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": ["None"] + } + ], + "technologyCostTrend": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Low or no change" + } + ], + "emissionsScope": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "CO2e (unspecified GHGs)" + } + ], + "policyAmbition": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "No policies included" + } + ], + "technologyCostsDetail": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Capital costs, O&M, etc." + } + ], + "newTechnologiesIncluded": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": ["No new technologies"] + } + ], + "investmentNeeds": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "By sector" + } + ] + }, + "coreDrivers": { + "policies": null, + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [] } diff --git a/src/data/asean-centre-for-energy/ACE-CNS-2024.json b/src/data/asean-centre-for-energy/ACE-CNS-2024.json index b3105738..87964791 100644 --- a/src/data/asean-centre-for-energy/ACE-CNS-2024.json +++ b/src/data/asean-centre-for-energy/ACE-CNS-2024.json @@ -1,5 +1,5 @@ { - "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v1.json", + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", "id": "ACE-CNS-2024", "publication": { "title": { @@ -73,7 +73,7 @@ "technologies": [] } ], - "expertOverview": "#### Pathway Description\n\nThe ASEAN Centre for Energy (ACE) acts as the ASEAN energy data center and knowledge hub and produces the ACE ASEAN Energy Outlook providing energy pathways for Southeast Asia. The Carbon Neutral Scenario (CNS) is a normative pathway in which the ASEAN region achieves net-zero carbon emissions by 2050, both in energy and non-energy sectors. This pathway builds on the extensive set of stated and aspirational policies modelled in ACE’s Regional Aspiration Scenario (RAS), introducing further emissions constraints, accelerated low-emissions technology and low-carbon fuels availability, gradual retirement of some coal and gas technologies, and expanded low-emission power generation. It assumes that countries improve their energy efficiency according to their full potential, develop and deploy renewable energy sources according to their individual technical potential, and models capacity additions beyond existing power development plans (PDPs), prioritizing dispatch of renewable energy. The CNS pathway also models widespread adoption of carbon capture and storage (CCS) technology, and rapid scale-up of low-carbon fuels. The CNS pathway incorporates least-cost-optimization in its projections.\n\nCore Drivers\n\nThe CNS pathway combines widespread policy, rapid technology deployment, and explicit targets for emissions and emissions intensity reductions.\n\n*Policy:* The policies considered by CNS are equivalent to the RAS pathway, including nationally determined contributions, energy efficiency targets, and power development plans. These policies are a core driver of CNS projections but not differentiate it from the RAS pathway. \n\n*Emissions goals:* The CNS pathway introduces explicit emissions constraints, with specific emissions and emissions intensity reduction targets in power, industry, and transport beyond what is contained in existing policy and pledges. These constraints impose rapid emissions reductions across the full energy system. \n\n*Technology Deployment & Technology costs:* Emissions constraints and the least-cost approach led to a more rapid build-out of low-carbon power capacity than in other ACE pathways, including a more rapid deployment of technologies that are less mature or not yet established in the region (such as nuclear, geothermal, and tidal & wave power). However, the CNS pathway does not forecast changes in technology costs, limiting the potential impact of cost optimization. \n\nThe CNS pathway provides detailed information on demand and investment flows. The pathway forecasts very large increases in energy investment, with 55 billion USD a year between 2023 and 2030, and up to 371 bn USD between 2040-2050, more than twice the investment seen in the ACE RAS pathway.\n\n#### Application to Transition Assessment\n\nThe Carbon Neutrality Scenario is a normative pathway, which provides a useful trajectory for exploring rapid and ambitious transition across Southeast Asia. As a single-region model, CNS is not linked to a global emissions outcome and so cannot be used to quantify corporate ambition in terms of implied temperature rise – however, as a pathway with large scale emissions reductions by 2050, it can be used as a region-specific benchmark for high ambition strategies.\n\nAs a normative pathway, CNS includes significant interventions that exceed existing regional goals. As a result, (mis)alignment to the CNS pathway is less directly connected to regulatory risk or potential shifts in market share, as objectives within the CNS pathway may differ significantly from currently stated jurisdictional policies. Users interested in assessing the policy alignment or technological and commercial feasibility of corporate transition strategies should supplement CNS with pathways that provide more direct links to predicted policy and market conditions, such as ACE RAS and IEA APS.\n\nAs a cost-optimized pathway, the CNS pathway can be used to quantify investment needs associated with a rapid region-wide change and to contextualize the commercial and market feasibility of a corporate transition strategy. However, due to exclusion of technology cost changes over time in CNS, and users may wish to supplement it with options that provide more detailed and dynamic cost and deployment projections.\n\nThe CNS pathway includes regional-level projections for power generation and capacity on 5-year intervals and using a moderately detailed breakdown by energy sources such as coal, wind, solar, biomass, and geothermal. These are relevant and useful for analyzing specific decarbonization levers and project investment pipelines within company plans. However, CNS provides data at only the regional level. This limits its suitability for assessing companies which have operations concentrated in one or few countries, as it allows comparison only to the regional average.", + "pathwayDescription": "The ASEAN Centre for Energy (ACE) acts as the ASEAN energy data center and knowledge hub and produces the ACE ASEAN Energy Outlook providing energy pathways for Southeast Asia. The Carbon Neutral Scenario (CNS) is a normative pathway in which the ASEAN region achieves net-zero carbon emissions by 2050, both in energy and non-energy sectors. This pathway builds on the extensive set of stated and aspirational policies modelled in ACE’s Regional Aspiration Scenario (RAS), introducing further emissions constraints, accelerated low-emissions technology and low-carbon fuels availability, gradual retirement of some coal and gas technologies, and expanded low-emission power generation. It assumes that countries improve their energy efficiency according to their full potential, develop and deploy renewable energy sources according to their individual technical potential, and models capacity additions beyond existing power development plans (PDPs), prioritizing dispatch of renewable energy. The CNS pathway also models widespread adoption of carbon capture and storage (CCS) technology, and rapid scale-up of low-carbon fuels. The CNS pathway incorporates least-cost-optimization in its projections.", "metric": [ "Emissions Intensity", "Capacity", @@ -82,27 +82,98 @@ "Absolute Emissions" ], "keyFeatures": { - "emissionsTrajectory": "Moderate decrease", - "energyEfficiency": "Significant improvement", - "energyDemand": "Minor increase", - "electrification": "Moderate increase", + "emissionsTrajectory": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Moderate decrease" + } + ], + "energyEfficiency": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Significant improvement" + } + ], + "energyDemand": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Minor increase" + } + ], + "electrification": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Moderate increase" + } + ], "policyTypes": [ - "Phaseout dates", - "Subsidies", - "Target technology shares", - "Performance standards", - "Other" + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": [ + "Phaseout dates", + "Subsidies", + "Target technology shares", + "Performance standards", + "Other" + ] + } + ], + "technologyCostTrend": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Low or no change" + } + ], + "emissionsScope": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "CO2e (unspecified GHGs)" + } + ], + "policyAmbition": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "High ambition policies" + } + ], + "technologyCostsDetail": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Capital costs, O&M, etc." + } ], - "technologyCostTrend": "Low or no change", - "emissionsScope": "CO2e (unspecified GHGs)", - "policyAmbition": "High ambition policies", - "technologyCostsDetail": "Capital costs, O&M, etc.", "newTechnologiesIncluded": [ - "Battery storage", - "CCUS", - "Green H2/ammonia", - "SAF" + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": ["Battery storage", "CCUS", "Green H2/ammonia", "SAF"] + } ], - "investmentNeeds": "By sector" - } + "investmentNeeds": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "By sector" + } + ] + }, + "coreDrivers": { + "policies": null, + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [] } diff --git a/src/data/asean-centre-for-energy/ACE-RAS-2024.json b/src/data/asean-centre-for-energy/ACE-RAS-2024.json index 4e57929e..abe9050b 100644 --- a/src/data/asean-centre-for-energy/ACE-RAS-2024.json +++ b/src/data/asean-centre-for-energy/ACE-RAS-2024.json @@ -1,5 +1,5 @@ { - "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v1.json", + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", "id": "ACE-RAS-2024", "publication": { "title": { @@ -72,7 +72,7 @@ "technologies": [] } ], - "expertOverview": "#### Pathway Description\n\nThe ASEAN Centre for Energy (ACE) acts as the ASEAN energy data center and knowledge hub and produces the ACE ASEAN Energy Outlook providing energy pathways for Southeast Asia. The ACE Regional Aspiration Scenario (RAS) is an exploratory pathway which assumes that every ASEAN member state fully achieves both its unconditional and conditional transition targets on time, including Nationally Determined Contributions (NDCs), national power development plans (PDPs), and the enhanced scenarios of the national energy roadmaps of each ASEAN member state (e.g., the Clean Energy Scenario in the Philippines). The RAS pathway includes the 2025 ASEAN Plan of Action for Energy Cooperation (APAEC) targets for renewable energy generation and energy efficiency. The RAS pathway extends the ACE ATS pathway, covering all policies included in ATS with the inclusion of additional aspirational goals. The RAS also differs from the ATS pathway in using a least-cost optimization approach, which results in both a faster build-out of renewable energy and lower utilization rates for fossil fuel power plans. \n\nEmissions remain stable in the RAS until 2030, after which they start to decrease very slightly at a rate of approximately 0.1% per year across the energy sector. Emissions from power generation decrease much more rapidly to approximately a third of 2022 emissions by 2050. Emissions in the industry and residential sectors also drop significantly compared to the ATS.\n\n#### Core Drivers\n\nThe RAS pathway is primarily driven by a range of policy mechanisms and cost optimization based on current technology costs.\n\n*Policy:* The RAS pathway adds conditional NDC targets, national targets from the enhanced scenario of the AMS energy roadmap, and APAEC 2025 regional targets on top of existing policies and unconditional NDCS (modeled in ACE ATS). This increases the target share of renewable energy in total energy production and requires a steeper reduction of energy intensity in transport and industry.\n\n*Technology Costs and Technology Deployment:* RAS incorporates a least-cost approach to power capacity additions and generation. This results in a continued shift towards lower-cost renewables generation and a reduction in utilization rates for fossil fuel power plants. However, the RAS pathway does not include potential changes in technology costs over time. \n\nThe RAS pathway provides information on demand shifts, investment flows, and technology deployment, but these factors are driven primarily by modeled policy shifts.\n\n#### Application to Transition Assessment\n\nACE RAS is an exploratory, policy-focused pathway, which provides useful values for assessing the alignment of corporate plans to region-wide policy impacts under conditions of significant policy action. As a region-specific pathway, ACE RAS is not directly linked to a global implied temperature rise, limiting its use as a quantitative benchmark for the ambition of climate targets. However, for companies that aim to align their strategies with national policy targets, the RAS can be used to assess the level of alignment between company ambition and relevant policy ambition.\n\nThe RAS is designed to support alignment assessments with potential future policies, making it a valuable tool for evaluating corporate strategies and investment pipelines against both national and regional ambitions. A misalignment with the RAS may signal potential exposure to future regulatory risks, such as non-compliance with evolving energy policies or reduced competitiveness in markets where low-carbon technologies are being prioritized. Alignment may indicate that a company is strategically positioned to benefit from market shifts. The RAS pathway includes the impacts of aspirational policies and targets which have not yet been implemented; therefore, misalignment does not necessarily imply exposure to current regulatory risk but rather a potential gap in future readiness. Due to its inclusion of aspirational goals, alignment to the RAS pathway gives a stronger indication that a company is keeping pace with national ambition than alignment to a more limited policy pathway such as ACE ATS.\n\nDue to its inclusion of least-cost-optimized projections, the RAS pathway has useful applications for assessing the commercial feasibility of transition strategies. Alignment or misalignment to the RAS pathway may indicate that a company is outpacing or lagging economy-wide optimal trends. However, due to RAS’s assumption of static technology costs, users may consider supplementing the RAS pathway with options that provide more detailed and dynamic technology cost and deployment projections.\n\nThe RAS pathway provides regional-level projections for generation and capacity on 5-year intervals and using a moderately detailed breakdown by energy sources such as coal, wind, solar, biomass, and geothermal. This level of detail makes it well-suited to assessing specific decarbonization levers and project pipelines within company plans. However, RAS provides data at only the regional level. This limits its suitability for assessing companies which have operations concentrated in one or few countries, as it allows comparison only to the regional average.", + "pathwayDescription": "The ASEAN Centre for Energy (ACE) acts as the ASEAN energy data center and knowledge hub and produces the ACE ASEAN Energy Outlook providing energy pathways for Southeast Asia. The ACE Regional Aspiration Scenario (RAS) is an exploratory pathway which assumes that every ASEAN member state fully achieves both its unconditional and conditional transition targets on time, including Nationally Determined Contributions (NDCs), national power development plans (PDPs), and the enhanced scenarios of the national energy roadmaps of each ASEAN member state (e.g., the Clean Energy Scenario in the Philippines). The RAS pathway includes the 2025 ASEAN Plan of Action for Energy Cooperation (APAEC) targets for renewable energy generation and energy efficiency. The RAS pathway extends the ACE ATS pathway, covering all policies included in ATS with the inclusion of additional aspirational goals. The RAS also differs from the ATS pathway in using a least-cost optimization approach, which results in both a faster build-out of renewable energy and lower utilization rates for fossil fuel power plans. \n\nEmissions remain stable in the RAS until 2030, after which they start to decrease very slightly at a rate of approximately 0.1% per year across the energy sector. Emissions from power generation decrease much more rapidly to approximately a third of 2022 emissions by 2050. Emissions in the industry and residential sectors also drop significantly compared to the ATS.", "metric": [ "Emissions Intensity", "Capacity", @@ -81,22 +81,98 @@ "Absolute Emissions" ], "keyFeatures": { - "emissionsTrajectory": "Low or no change", - "energyEfficiency": "Significant improvement", - "energyDemand": "Moderate increase", - "electrification": "Low or no change", + "emissionsTrajectory": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Low or no change" + } + ], + "energyEfficiency": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Significant improvement" + } + ], + "energyDemand": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Moderate increase" + } + ], + "electrification": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Low or no change" + } + ], "policyTypes": [ - "Phaseout dates", - "Subsidies", - "Target technology shares", - "Performance standards", - "Other" + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": [ + "Phaseout dates", + "Subsidies", + "Target technology shares", + "Performance standards", + "Other" + ] + } + ], + "technologyCostTrend": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Low or no change" + } ], - "technologyCostTrend": "Low or no change", - "emissionsScope": "CO2e (unspecified GHGs)", - "policyAmbition": "NDCs incl. conditional targets", - "technologyCostsDetail": "Capital costs, O&M, etc.", - "newTechnologiesIncluded": ["Battery storage", "Green H2/ammonia", "SAF"], - "investmentNeeds": "By sector" - } + "emissionsScope": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "CO2e (unspecified GHGs)" + } + ], + "policyAmbition": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "NDCs incl. conditional targets" + } + ], + "technologyCostsDetail": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "Capital costs, O&M, etc." + } + ], + "newTechnologiesIncluded": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": ["Battery storage", "Green H2/ammonia", "SAF"] + } + ], + "investmentNeeds": [ + { + "sector": "cross-sector", + "geography": "South East Asia", + "value": "By sector" + } + ] + }, + "coreDrivers": { + "policies": null, + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [] } diff --git a/src/data/iea/IEA-APS-2024.json b/src/data/iea/IEA-APS-2024.json index 64bdfa8b..7e17eeff 100644 --- a/src/data/iea/IEA-APS-2024.json +++ b/src/data/iea/IEA-APS-2024.json @@ -1,5 +1,5 @@ { - "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v1.json", + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", "id": "IEA-APS-2024", "publication": { "title": { @@ -316,8 +316,7 @@ "technologies": [] } ], - "pathwayOverview": "The Announced Pledges Scenario (APS) shows the future of the energy sector if all countries were to hit their aspirational targets, including national and regional net zero emissions pledges, on time and in full – in addition to their legislated policies (as described in STEPS). The APS has a 50% probability to not exceed 1.7°C by 2100. The difference between the APS and the Net Zero Emissions by 2050 scenario highlights the ambition gap between countries’ commitments and a 1.5°C pathway.", - "expertOverview": "#### Pathway Description\n\nThe Announced Pledges Scenario (APS) provides a pathway in which all countries achieve their aspirational energy transition goals, including national and regional net zero emissions pledges, in addition to their currently legislated policies. As an exploratory scenario, APS provides a detailed description of a possible future, but does not attempt to predict which announced policies will be implemented. The APS pathway corresponds to 1.7°C of warming by 2100 (50% probability). This is significantly lower than the IEA’s current policies scenario (STEPS), which forecasts 2.4°C of warming by 2100 (50% probability), but still higher than the IEA’s Net Zero scenario (NZE), which targets 1.5°C of warming by 2100 (50% probability). This indicates that the policies and pledges incorporated into APS significantly increase the pace of the transition compared to status quo conditions.\n\n#### Core Drivers\n\nIEA APS is primarily driven by large-scale policy action, continued cost decline for low-emissions technologies, and the introduction of new low-carbon technology.\n\n*Policy:* The APS assumes current nationally determined contributions (NDCs) are met, including, for example, emissions intensity goals and coal phase-out commitments. It includes national power development plans and targets for renewable energy additions or technology-based capacity mixes. In addition to explicit policies included in pledges, APS introduces additional measures to achieve aspirational policy goals. The APS models a carbon price, affecting electricity, industry and energy production sectors. The carbon tax is structured in three regional tiers: advanced economies, emerging markets and developing economies (EMDEs) with net-zero pledges, and EMDEs without pledges. \n\n*Technology costs:* IEA APS assumes significant cost declines in solar PV, onshore wind, and offshore wind power generation in most regions of the world and modest cost declines in nuclear power generation. These declines are mostly driven by reduced capital costs. The combined costs of fuel, CO2 prices, and operations and maintenance are projected to increase, making fossil-fuel based electricity less competitive over time.\n\nThe APS pathway provides detailed projections for demand shifts, infrastructure buildout, investment flow changes, and new technology deployment, but these factors are primarily caused by modelled policies and declining technology costs.\n\n#### Application to Transition Assessment\n\nAPS is a detailed, policy-focused, exploratory pathway. As a global integrated assessment model, APS provides a direct connection between the described pathway and a global temperature outcome. This makes it suitable for assessing the ambition of targets or plans against a 1.7°C outcome.\n\nDue to its strong policy focus, APS is well-suited to assessing the alignment of corporate targets, plans, and investment pipelines with the stated ambitions of the jurisdictions they operate in. Misalignment to the APS pathway can indicate potential risk that a company will fall out of line with future regulatory or economic policy and face declining market share as other segments of the energy sector are encouraged to grow. Conversely, alignment to APS may indicate that a company is well-positioned to take advantage of future policy and market dynamics. However, because APS includes aspirational pledges and policies which have not yet been implemented, misalignment to APS does not mean a company is automatically exposed to current regulatory risk.\n \nAPS provides benchmark data on 5- and 10-year intervals and uses a moderately detailed breakdown of specific generation technologies such as coal, onshore wind, offshore wind, solar PV, and geothermal. This level of detail makes it well-suited to assessing specific decarbonization levers and project pipelines within company plans. However, APS provides data at only the regional level, which limits its suitability for assessing companies which have operations concentrated in one or few countries, as it allows comparison only to the regional average.", + "pathwayDescription": "The Announced Pledges Scenario (APS) provides a pathway in which all countries achieve their aspirational energy transition goals, including national and regional net zero emissions pledges, in addition to their currently legislated policies. As an exploratory scenario, APS provides a detailed description of a possible future, but does not attempt to predict which announced policies will be implemented. The APS pathway corresponds to 1.7°C of warming by 2100 (50% probability). This is significantly lower than the IEA’s current policies scenario (STEPS), which forecasts 2.4°C of warming by 2100 (50% probability), but still higher than the IEA’s Net Zero scenario (NZE), which targets 1.5°C of warming by 2100 (50% probability). This indicates that the policies and pledges incorporated into APS significantly increase the pace of the transition compared to status quo conditions.", "metric": [ "Emissions Intensity", "Capacity", @@ -326,29 +325,99 @@ "Absolute Emissions" ], "keyFeatures": { - "emissionsTrajectory": "Moderate decrease", - "energyEfficiency": "Significant improvement", - "energyDemand": "Low or no change", - "electrification": "Moderate increase", + "emissionsTrajectory": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Moderate decrease" + } + ], + "energyEfficiency": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Significant improvement" + } + ], + "energyDemand": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Low or no change" + } + ], + "electrification": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Moderate increase" + } + ], "policyTypes": [ - "Carbon price", - "Phaseout dates", - "Subsidies", - "Target technology shares", - "Performance standards", - "Other" + { + "sector": "cross-sector", + "geography": "Global", + "value": [ + "Carbon price", + "Phaseout dates", + "Subsidies", + "Target technology shares", + "Performance standards", + "Other" + ] + } + ], + "technologyCostTrend": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Decrease" + } + ], + "emissionsScope": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "CO2" + } + ], + "policyAmbition": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "NDCs incl. conditional targets" + } + ], + "technologyCostsDetail": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Capital costs, O&M, etc." + } ], - "technologyCostTrend": "Decrease", - "emissionsScope": "CO2", - "policyAmbition": "NDCs incl. conditional targets", - "technologyCostsDetail": "Capital costs, O&M, etc.", "newTechnologiesIncluded": [ - "CCUS", - "DAC", - "Green H2/ammonia", - "SAF", - "Battery storage" + { + "sector": "cross-sector", + "geography": "Global", + "value": ["CCUS", "DAC", "Green H2/ammonia", "SAF", "Battery storage"] + } ], - "investmentNeeds": "By tech, part of value chain" - } + "investmentNeeds": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "By tech, part of value chain" + } + ] + }, + "coreDrivers": { + "policies": null, + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [] } diff --git a/src/data/iea/IEA-NZE-2024.json b/src/data/iea/IEA-NZE-2024.json index 5aa1447e..e4b358d6 100644 --- a/src/data/iea/IEA-NZE-2024.json +++ b/src/data/iea/IEA-NZE-2024.json @@ -1,5 +1,5 @@ { - "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v1.json", + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", "id": "IEA-NZE-2024", "publication": { "title": { @@ -98,7 +98,7 @@ "technologies": [] } ], - "expertOverview": "#### Pathway Description\n\nThe IEA Net Zero Emissions by 2050 (NZE) Scenario is a normative 1.5 aligned pathway, outlining how the global energy sector can reach net zero CO2 emissions by 2050. It assumes that advanced economies achieve net zero earlier (by mid 2040s), with emerging markets following by 2050. As a normative scenario, the NZE provides a detailed description of one pathway to reach a targeted future state, but it does not attempt to predict which policies or outcomes are most probable. The NZE pathway corresponds to a 50% probability of 1.5°C of warming by 2100. This is a more ambitious outcome than the IEA’s Announced Pledges Scenario (APS), which projects 1.7°C of warming by 2100 (50% probability), indicating a significant gap remains between announced governmental pledges and the trajectory described by the NZE pathway.\n\n#### Core Drivers\n\nThe NZE pathway is driven through a combination of wide-reaching policy, significant cost declines for existing green technologies, significant efficiency improvement and demand shifts, and the introduction of significant new technologies.\n\n*Policy:* The NZE pathway expands upon the policies considered in the APS pathway, most notably by introducing a more significant global carbon price for power, industry, and transport. A carbon price is introduced in all regions by 2030, with prices rising to USD$250/tCO2 in advanced economies, and USD$200/tCO2 in major emerging markets by 2050. Additionally, while the IEA NZE makes note of how the phase-out of fossil fuel subsidies need to be carefully designed to limit impacts on household budgets, they are largely removed in the NZE by 2030.\n\n*Technology costs:* The IEA NZE pathway assumes an S-curve trajectory of cost decline, with rapid early reductions as deployment scales up, followed by gradual flattening. This pattern applies across most renewables and transition technologies, including solar PV, offshore wind, battery EVs, and hydrogen fuel cells.\n\n*Technology shifts:* The NZE pathway is primarily driven by the rapid deployment of clean technology. Renewable energy capacity, led by solar PV and wind, approximately triples between 2023 and 2030, and reaches nearly 90% of global electricity by 2050. EVs account for approximately 60% of new car sales by 2030, and dominate the global fleet by 2050. Additionally, CCUS scales from 40Mt in 2023 to over 1Gt in 2030, and over 6Gt in 2050. This rapid expansion of clean technologies enables the peak of unabated fossil-fuel demand before 2030 and its subsequent decline by about 80% by 2050, marking the global phase down of coal, oil, and gas. \n\n*Falling energy demand:* The NZE pathway models significant energy efficiency improvements, alongside meaningful shifts in individual consumption patterns, which significantly reduce potential energy demand. Total energy consumption in the NZE pathway falls from well over 400 EJ in 2023 to under 350 EJ in 2050, a much larger decline than is observed in most similar pathways.\n\n#### Application to Transition Assessment\n\nIEA NZE is a global, temperature constrained normative scenario. As such, the NZE provides a direct connection between the described pathway and a global temperature outcome. It is well-suited for science-based target setting and net zero strategy alignment and is one of the most widely-used pathways to benchmark strategies against a projected 1.5°C temperature rise.\n\nAs a normative pathway, NZE models a potential pathway to achieve a target temperature outcome and introduces significant new policies and market shifts as part of the process. As a result, (mis)alignment to the NZE pathway is less directly connected to regulatory risk or potential shifts in market share, as objectives within the NZE pathway may differ significantly from currently stated jurisdictional goals. Users interested in assessing the policy alignment or technological and commercial feasibility of corporate transition strategies should review where underlying NZE assumptions diverge from current trends, and consider supplementing NZE with additional pathways that model a range of different policies and technology developments.\n\nThe NZE provides benchmark data on 5- and 10-year intervals and uses a moderately detailed breakdown of specific generation technologies such as coal, onshore wind, offshore wind, solar PV, and geothermal. This level of detail makes it well-suited to assessing specific decarbonization levers. However, NZE provides data only at the global level. This limits its suitability for assessing region- or jurisdiction-specific implications for company transition strategies.", + "pathwayDescription": "The IEA Net Zero Emissions by 2050 (NZE) Scenario is a normative 1.5 aligned pathway, outlining how the global energy sector can reach net zero CO2 emissions by 2050. It assumes that advanced economies achieve net zero earlier (by mid 2040s), with emerging markets following by 2050. As a normative scenario, the NZE provides a detailed description of one pathway to reach a targeted future state, but it does not attempt to predict which policies or outcomes are most probable. The NZE pathway corresponds to a 50% probability of 1.5°C of warming by 2100. This is a more ambitious outcome than the IEA’s Announced Pledges Scenario (APS), which projects 1.7°C of warming by 2100 (50% probability), indicating a significant gap remains between announced governmental pledges and the trajectory described by the NZE pathway.", "metric": [ "Emissions Intensity", "Capacity", @@ -107,29 +107,99 @@ "Absolute Emissions" ], "keyFeatures": { - "emissionsTrajectory": "Significant decrease", - "energyEfficiency": "Significant improvement", - "energyDemand": "Significant decrease", - "electrification": "Significant increase", + "emissionsTrajectory": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Significant decrease" + } + ], + "energyEfficiency": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Significant improvement" + } + ], + "energyDemand": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Significant decrease" + } + ], + "electrification": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Significant increase" + } + ], "policyTypes": [ - "Carbon price", - "Phaseout dates", - "Subsidies", - "Target technology shares", - "Performance standards", - "Other" + { + "sector": "cross-sector", + "geography": "Global", + "value": [ + "Carbon price", + "Phaseout dates", + "Subsidies", + "Target technology shares", + "Performance standards", + "Other" + ] + } + ], + "technologyCostTrend": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Decrease" + } + ], + "emissionsScope": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "CO2" + } + ], + "policyAmbition": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "High ambition policies" + } + ], + "technologyCostsDetail": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Capital costs, O&M, etc." + } ], - "technologyCostTrend": "Decrease", - "emissionsScope": "CO2", - "policyAmbition": "High ambition policies", - "technologyCostsDetail": "Capital costs, O&M, etc.", "newTechnologiesIncluded": [ - "CCUS", - "DAC", - "Green H2/ammonia", - "SAF", - "Battery storage" + { + "sector": "cross-sector", + "geography": "Global", + "value": ["CCUS", "DAC", "Green H2/ammonia", "SAF", "Battery storage"] + } ], - "investmentNeeds": "By tech, part of value chain" - } + "investmentNeeds": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "By tech, part of value chain" + } + ] + }, + "coreDrivers": { + "policies": null, + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [] } diff --git a/src/data/iea/IEA-STEPS-2024.json b/src/data/iea/IEA-STEPS-2024.json index 414ae794..3db36047 100644 --- a/src/data/iea/IEA-STEPS-2024.json +++ b/src/data/iea/IEA-STEPS-2024.json @@ -1,5 +1,5 @@ { - "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v1.json", + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", "id": "IEA-STEPS-2024", "publication": { "title": { @@ -316,8 +316,7 @@ "technologies": [] } ], - "pathwayOverview": "The Stated Policies Scenario (STEPS) provides a sense of the energy sector’s direction of travel today, based on the latest market data, technology costs and in-depth analysis of the prevailing policy settings in countries around the world. STEPS is complemented by APS, a scenario which assumes that countries’ aspirational targets will be met in full. The difference between STEPS and APS highlights a commitment gap.", - "expertOverview": "#### Pathway Description\n\nThe Stated Policies Scenario (STEPS) projects the energy sector’s current direction of travel, based on the latest market data, technology costs and in-depth analysis of the stated policies in countries around the world. STEPS provides a detailed projection of future outcomes based on current conditions, but does not attempt to predict which existing policies or economic conditions are more or less likely to change. The STEPS pathway corresponds to 2.4°C of warming by 2100 (50% probability). This is significantly higher than the IEA’s announced pledges scenario (APS), which projects 1.7°C of warming by 2100 (50% probability), and the IEA’s Net Zero scenario (NZE), which targets 1.5°C of warming by 2100 (50% probability).\n\n#### Core Drivers\n\nThe STEPS pathway is primarily driven by existing policy dynamics and forecast future declines in technology costs, which drive large-scale deployment of mature low-carbon technologies.\n\n*Policy:* The STEPS pathway models stated policies, such as energy subsidies or power development plans, but does not include aspirational policy goals that lack specific provisions for their implementation, such as long-term net-zero pledges. The STEPS pathway includes only existing or scheduled carbon pricing schemes for electricity, industry, and energy production sectors, and does not introduce a global carbon price, as is done in the APS and NZE pathway.\n\n*Technology costs:* Depending on the region, the IEA STEPS assumes moderate to significant cost declines in solar PV, onshore wind, and offshore wind power generation and modest cost declines in nuclear power generation. These declines are mostly driven by falling capital costs. The pathway projects moderate increases in fossil fuel power generation costs in advanced economies, but mixed trends in EMDEs. \n\nThe STEPS pathway provides detailed projections for demand shifts, infrastructure buildout, investment flows, and new technology deployment, but these factors are primarily caused by modelled existing policies and declining technology costs. \n\n#### Application to Transition Assessment\n\nAs a predictive pathway based on detailed modeling of current stated policies, market conditions, and technology trends, STEPS provides a valuable base-case projection for benchmarking the impact of future transition impacts in the absence of significant policy or market shifts. As a global integrated assessment model, STEPS provides a direct connection between the described pathway and a global temperature outcome. While this allows assessing the ambition of stated targets or plans against a 2.4°C outcome, this high level of implied temperature rise is not applicable for most institutional targets.\n\nDue to its strong focus on modeling existing policy and market trends, STEPS is well-suited to assessing the alignment of corporate targets, plans, and investment pipelines against a conservative forecast of market shifts. Misalignment to the STEPS pathway indicates that a company or plan may already be lagging forecasted rates of change, potentially exposing them to regulatory risks or declining market share as other segments of the energy sector grow. As STEPS only models stated policies with clear implementation plans, and does not include aspirational pledges such as those modeled in APS, it can be viewed as a conservative projection of potential market dynamics, and plans that exceed the rate of transition forecast in STEPS may still be exposed to future risks if new policies or technology shifts occur.\n\nSTEPS provides benchmark data on 5- and 10-year intervals and uses a moderately detailed breakdown of specific generation technologies such as coal, onshore wind, offshore wind, solar PV, and geothermal. This level of detail makes it well-suited to assessing specific decarbonization levers and project pipelines within company plans. However, STEPS provides data at only the regional level. This limits its suitability for assessing companies which have operations concentrated in one or few countries, as it allows comparison only to the regional average.", + "pathwayDescription": "The Stated Policies Scenario (STEPS) projects the energy sector’s current direction of travel, based on the latest market data, technology costs and in-depth analysis of the stated policies in countries around the world. STEPS provides a detailed projection of future outcomes based on current conditions, but does not attempt to predict which existing policies or economic conditions are more or less likely to change. The STEPS pathway corresponds to 2.4°C of warming by 2100 (50% probability). This is significantly higher than the IEA’s announced pledges scenario (APS), which projects 1.7°C of warming by 2100 (50% probability), and the IEA’s Net Zero scenario (NZE), which targets 1.5°C of warming by 2100 (50% probability).", "metric": [ "Emissions Intensity", "Capacity", @@ -326,29 +325,99 @@ "Absolute Emissions" ], "keyFeatures": { - "emissionsTrajectory": "Minor decrease", - "energyEfficiency": "Moderate improvement", - "energyDemand": "Moderate increase", - "electrification": "Moderate increase", + "emissionsTrajectory": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Minor decrease" + } + ], + "energyEfficiency": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Moderate improvement" + } + ], + "energyDemand": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Moderate increase" + } + ], + "electrification": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Moderate increase" + } + ], "policyTypes": [ - "Carbon price", - "Phaseout dates", - "Subsidies", - "Target technology shares", - "Performance standards", - "Other" + { + "sector": "cross-sector", + "geography": "Global", + "value": [ + "Carbon price", + "Phaseout dates", + "Subsidies", + "Target technology shares", + "Performance standards", + "Other" + ] + } + ], + "technologyCostTrend": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Decrease" + } + ], + "emissionsScope": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "CO2" + } + ], + "policyAmbition": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Current and drafted policies" + } + ], + "technologyCostsDetail": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Capital costs, O&M, etc." + } ], - "technologyCostTrend": "Decrease", - "emissionsScope": "CO2", - "policyAmbition": "Current and drafted policies", - "technologyCostsDetail": "Capital costs, O&M, etc.", "newTechnologiesIncluded": [ - "CCUS", - "DAC", - "Green H2/ammonia", - "SAF", - "Battery storage" + { + "sector": "cross-sector", + "geography": "Global", + "value": ["CCUS", "DAC", "Green H2/ammonia", "SAF", "Battery storage"] + } ], - "investmentNeeds": "By tech, part of value chain" - } + "investmentNeeds": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "By tech, part of value chain" + } + ] + }, + "coreDrivers": { + "policies": null, + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [] } diff --git a/src/data/pathwayMetadata.ts b/src/data/pathwayMetadata.ts index 14129fb9..d02ec5ab 100644 --- a/src/data/pathwayMetadata.ts +++ b/src/data/pathwayMetadata.ts @@ -1,7 +1,12 @@ import { PathwayMetadataType } from "../types"; import { FileEntry } from "../utils/validateData"; -import { assembleData, decideIncludeInvalid } from "../utils/loadData"; -import pathwayMetadataSchema from "../schema/pathwayMetadata.v1.json" with { type: "json" }; +import { + assembleData, + decideIncludeInvalid, + isViteDev, +} from "../utils/loadData"; +import pathwayMetadataSchema from "../schema/pathwayMetadata.v2.json" with { type: "json" }; +import pathwayMetadataV1Schema from "../schema/pathwayMetadata.v1.json" with { type: "json" }; import { commonSchemas } from "../schema/common"; // 1) Grab every JSON file in this folder **and subfolders** @@ -20,6 +25,40 @@ const entries: FileEntry[] = Object.entries(modules) })) .sort((a, b) => a.name.localeCompare(b.name)); +/** + * Count metadata files still carrying the v1 `$schema` (#858). + * + * `validateDataCollect` routes each document by its own `$schema` and drops + * anything that does not match the schema it was handed — as neither valid nor + * invalid. That is what lets v1 and v2 documents share src/data during the + * migration, but it also means an un-migrated file vanishes from the app with no + * error at all. Counting them here turns "pathways are missing" from a mystery + * into a number. Timeseries files are routed away by the same mechanism and are + * not counted, since their absence from this list is by design. + * + * Dev-server only. The message names an internal script and issue number, which + * is useful to us and noise to anyone reading a production console — and this + * module otherwise does no logging at all (assembleData warns only when a caller + * opts in via `opts.warn`, which this one does not). + */ +const V1_METADATA_ID = String( + (pathwayMetadataV1Schema as { $id?: string }).$id, +); + +const unmigrated = entries.filter( + (e) => + typeof e.data === "object" && + e.data !== null && + (e.data as { $schema?: unknown }).$schema === V1_METADATA_ID, +).length; + +if (unmigrated > 0 && isViteDev()) { + console.warn( + `[pathwayMetadata] ${unmigrated} metadata file(s) still use schema v1 and are ` + + `not loaded. Migrate them with scripts/codemod-v1-to-v2.ts (#858).`, + ); +} + export const pathwayMetadata: PathwayMetadataType[] = assembleData( entries, pathwayMetadataSchema, diff --git a/src/pages/ComparisonPage.test.tsx b/src/pages/ComparisonPage.test.tsx index 3c763a54..5333ff04 100644 --- a/src/pages/ComparisonPage.test.tsx +++ b/src/pages/ComparisonPage.test.tsx @@ -24,7 +24,11 @@ const fixtures = [ sectors: [{ name: "Power" }], metric: ["Capacity"], geography: { global: true, regions: { Europe: [] }, country: ["US"] }, - keyFeatures: { emissionsTrajectory: "foo" }, + keyFeatures: { + emissionsTrajectory: [ + { sector: "cross-sector", geography: "Global", value: "foo" }, + ], + }, }, { id: "cmp-b", @@ -41,7 +45,11 @@ const fixtures = [ sectors: [{ name: "Steel" }], metric: ["Generation"], geography: { country: ["DE", "FR"] }, - keyFeatures: { emissionsTrajectory: "bar" }, + keyFeatures: { + emissionsTrajectory: [ + { sector: "cross-sector", geography: "DE", value: "bar" }, + ], + }, }, ] as const; diff --git a/src/pages/PathwayDetailPage.tsx b/src/pages/PathwayDetailPage.tsx index e802cd90..42509a31 100644 --- a/src/pages/PathwayDetailPage.tsx +++ b/src/pages/PathwayDetailPage.tsx @@ -261,8 +261,17 @@ const PathwayDetailPage: React.FC = () => {

Expert Overview

+ {/* + v1's single `expertOverview` blob held three sections. In v2 + only the description survives as prose: "Core Drivers" becomes + the structured `coreDrivers` object, and "Application to + Transition Assessment" is retired because the new UI does not + display it (#898). So this renders one field, not a set of + sub-headed blocks. #859 owns the real presentation, including + whether this heading keeps its name. + */}
- {pathway.expertOverview} + {pathway.pathwayDescription ?? ""}
diff --git a/src/pages/PathwaySearch.test.tsx b/src/pages/PathwaySearch.test.tsx index 7a04b0e0..c1e38172 100644 --- a/src/pages/PathwaySearch.test.tsx +++ b/src/pages/PathwaySearch.test.tsx @@ -65,7 +65,11 @@ describe("PathwaySearch integration: dropdowns render and filter with 'None'", ( pathwayType: "Net Zero", modelYearNetzero: 2050, metric: [], - keyFeatures: { emissionsTrajectory: "foo" }, + keyFeatures: { + emissionsTrajectory: [ + { sector: "cross-sector", geography: "DE", value: "foo" }, + ], + }, }, { id: "B", @@ -78,7 +82,11 @@ describe("PathwaySearch integration: dropdowns render and filter with 'None'", ( pathwayType: "Net Zero", modelYearNetzero: 2050, metric: ["Capacity"], - keyFeatures: { emissionsTrajectory: "foo" }, + keyFeatures: { + emissionsTrajectory: [ + { sector: "cross-sector", geography: "DE", value: "foo" }, + ], + }, }, { id: "C", @@ -89,7 +97,9 @@ describe("PathwaySearch integration: dropdowns render and filter with 'None'", ( pathwayType: "NZi2050", modelYearNetzero: 2040, metric: [], - keyFeatures: { emissionsTrajectory: "foo" }, + keyFeatures: { + emissionsTrajectory: [], // no geography declared -> no scope to attach a value to + }, }, { id: "D", @@ -100,7 +110,11 @@ describe("PathwaySearch integration: dropdowns render and filter with 'None'", ( pathwayType: "BAU", modelYearNetzero: 2030, metric: ["Capacity", "Generation"], - keyFeatures: { emissionsTrajectory: "bar" }, + keyFeatures: { + emissionsTrajectory: [ + { sector: "cross-sector", geography: "JP", value: "bar" }, + ], + }, }, { id: "E", @@ -111,7 +125,11 @@ describe("PathwaySearch integration: dropdowns render and filter with 'None'", ( pathwayType: "Net Zero", modelYearNetzero: 2050, metric: ["Generation"], - keyFeatures: { emissionsTrajectory: "bar" }, + keyFeatures: { + emissionsTrajectory: [ + { sector: "cross-sector", geography: "DE", value: "bar" }, + ], + }, }, ] as const; diff --git a/src/schema/common/dataAvailability.v1.json b/src/schema/common/dataAvailability.v1.json new file mode 100644 index 00000000..1c8065f4 --- /dev/null +++ b/src/schema/common/dataAvailability.v1.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://pathways.rmi.org/schema/common/dataAvailability.v1.json", + "title": "Data Availability", + "description": "The closed vocabularies describing how a metric's underlying data can be obtained (#870).", + "$comment": "A wrapper object whose only job is to keep every $def reachable from the root: generate-types.ts runs json-schema-to-typescript with unreachableDefinitions:false, so a $def nothing points at emits no type. Same pattern as technology.v1.json and metric.v1.json. Callers $ref the individual $defs, not this object.", + "type": "object", + "properties": { + "geographyCoverage": { "$ref": "#/$defs/geographyCoverage" }, + "timeResolution": { "$ref": "#/$defs/timeResolution" }, + "dataFormat": { "$ref": "#/$defs/dataFormat" }, + "access": { "$ref": "#/$defs/access" } + }, + "required": ["geographyCoverage", "timeResolution", "dataFormat", "access"], + "additionalProperties": false, + + "$defs": { + "geographyCoverage": { + "description": "How geographically granular the underlying data is.", + "$comment": "A coverage class, NOT a scope. It says the data resolves to individual countries; it does not say which. The entry's sibling 'geography' carries the scope.", + "type": "string", + "enum": ["Global", "Regional", "Country"] + }, + "timeResolution": { + "description": "How finely the underlying data is resolved over time.", + "type": "string", + "enum": [ + "No information", + "Single year (2050)", + "Medium-term", + "10-year", + "5-10-year", + "5-year", + "1-year", + "Other" + ] + }, + "dataFormat": { + "description": "Where the data can be obtained, and in what form.", + "$comment": "#870 specifies this as one enum with parenthetical free|paywall ('tabular-in-pub (free|paywall)'). Split into dataFormat and access because the acceptance criteria call for the tables/text indicator and the free/paywall indicator as separate UI signals; a flat five-member enum would make the table string-match to recover them. 'In tool' means we host it -- those rows link to the timeseries download via the generated index. A downloadable non-timeseries value is expected here once the hosting mechanism is decided.", + "type": "string", + "enum": ["In tool", "Tabular in publication", "Text in publication"] + }, + "access": { + "description": "Whether reaching the data at the publisher costs money. Null when we host it.", + "$comment": "Null exactly when dataFormat is 'In tool' -- a paywall on data we serve ourselves is a contradiction. draft-07 cannot tie two sibling properties together, so validateScopedEntries enforces the biconditional.", + "type": ["string", "null"], + "enum": ["Free", "Paywalled", null] + } + } +} diff --git a/src/schema/common/index.ts b/src/schema/common/index.ts index d7b9debc..f176a79f 100644 --- a/src/schema/common/index.ts +++ b/src/schema/common/index.ts @@ -28,6 +28,20 @@ export const geographySchema: SchemaObject = geographySchemaJson; import emissionsScopeSchemaJson from "./emissionsScope.v1.json" with { type: "json" }; export const emissionsScopeSchema: SchemaObject = emissionsScopeSchemaJson; +// The two axes of a v2 scoped keyFeatures entry (#858). +import scopeSectorSchemaJson from "./scopeSector.v2.json" with { type: "json" }; +export const scopeSectorSchema: SchemaObject = scopeSectorSchemaJson; + +import scopeGeographySchemaJson from "./scopeGeography.v2.json" with { type: "json" }; +export const scopeGeographySchema: SchemaObject = scopeGeographySchemaJson; + +// Vocabularies for the per-metric dataAvailability entries (#870). +import sectorSegmentSchemaJson from "./sectorSegment.v1.json" with { type: "json" }; +export const sectorSegmentSchema: SchemaObject = sectorSegmentSchemaJson; + +import dataAvailabilitySchemaJson from "./dataAvailability.v1.json" with { type: "json" }; +export const dataAvailabilitySchema: SchemaObject = dataAvailabilitySchemaJson; + // Aggregate — type stays correct export const commonSchemas: SchemaObject[] = [ publicationSchema, @@ -39,6 +53,10 @@ export const commonSchemas: SchemaObject[] = [ countryCodeSchema, geographySchema, emissionsScopeSchema, + scopeSectorSchema, + scopeGeographySchema, + sectorSegmentSchema, + dataAvailabilitySchema, ]; export default commonSchemas; diff --git a/src/schema/common/scopeGeography.v2.json b/src/schema/common/scopeGeography.v2.json new file mode 100644 index 00000000..4e787051 --- /dev/null +++ b/src/schema/common/scopeGeography.v2.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "title": "Scope Geography", + "description": "The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label.", + "$comment": "Deliberately an open string rather than an enum. Region labels are free-form by design -- geography.v1's 'regions' keys are kept as named in the source publication -- so a closed list could not express labels this repo already uses, e.g. IEA's 'Asia Pacific', 'Eurasia', 'Middle East', 'Central and South America', none of which appear in geographyItem.v1's enum. (#858's scope text says geographyItem, which would reject any finer-scope entry on an IEA pathway.) 'Global' is the widest sentinel; 'cross-region' is reserved for a multi-region non-global aggregate. Because any non-blank string validates here, the real constraint -- that the value is 'Global', 'cross-region', or a region/country the pathway declares in its own 'geography' -- is enforced by validateScopedEntries in src/utils/validateScopes.ts, run from scripts/schema-check-files.ts. That check is what catches a mistyped label.", + "type": "string", + "minLength": 1, + "pattern": "^(?!\\s*$).+", + "not": { + "type": "string", + "pattern": "^[A-Za-z]{3}$" + }, + "examples": [ + "Global", + "cross-region", + "South East Asia", + "Asia Pacific", + "TH", + "US" + ] +} diff --git a/src/schema/common/scopeSector.v2.json b/src/schema/common/scopeSector.v2.json new file mode 100644 index 00000000..f6068dad --- /dev/null +++ b/src/schema/common/scopeSector.v2.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "title": "Scope Sector", + "description": "The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'.", + "$comment": "Members mirror sector.v1.json#/$defs/displayName, plus the 'cross-sector' sentinel. 'cross-sector' means the union of THIS pathway's own declared sectors, not a universal match: an entry scoped 'cross-sector' covers a query for sector X only when X is one of the sectors the pathway declares in its own 'sectors' array. So a pathway covering only Y and Z is NOT a match for a sector-X search, even though its 'cross-sector' values would otherwise be broad enough. That constraint spans sibling data with dynamic keys, which draft-07 cannot express and AJV's $data cannot compute a union for, so it is enforced by validateScopedEntries in src/utils/validateScopes.ts, run from scripts/schema-check-files.ts.", + "type": "string", + "enum": [ + "cross-sector", + "Land Use", + "Agriculture", + "Buildings", + "Steel", + "Cement", + "Chemicals", + "Coal Mining", + "Oil (Upstream)", + "Gas (Upstream)", + "Power", + "Automotive", + "Aviation", + "Rail", + "Shipping", + "Other" + ], + "examples": ["cross-sector", "Power", "Steel"] +} diff --git a/src/schema/common/sectorSegment.v1.json b/src/schema/common/sectorSegment.v1.json new file mode 100644 index 00000000..77c8ba7c --- /dev/null +++ b/src/schema/common/sectorSegment.v1.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://pathways.rmi.org/schema/common/sectorSegment.v1.json", + "title": "Sector Segment", + "description": "Segments within a sector", + "type": "object", + "properties": { + "displayName": { "$ref": "#/$defs/displayName" } + }, + "required": ["displayName"], + "additionalProperties": false, + + "$defs": { + "displayName": { + "description": "Display name of the sector segment as presented in tables.", + "$comment": "Segments are sector-specific: 'Storage' is meaningful for Power and meaningless for Land Use. draft-07 cannot condition this enum on a sibling 'sector' value -- and the if/then form that could collapses the generated object type to an index signature (see validateScopes.ts) -- so membership is enforced by validateScopedEntries against SECTORS_BY_KEY in src/utils/timeseriesTaxonomy.ts. Only Power's segments are defined so far; 'No information' is legal under every sector, so a pathway in a sector whose segments nobody has written down yet can still record data availability.", + "type": "string", + "enum": [ + "No information", + "Power generation", + "Storage", + "Transmission & Distribution" + ] + } + } +} diff --git a/src/schema/pathwayMetadata.v1.json b/src/schema/pathwayMetadata.v1.json index f5daf1ad..d60af8ac 100644 --- a/src/schema/pathwayMetadata.v1.json +++ b/src/schema/pathwayMetadata.v1.json @@ -32,7 +32,7 @@ "tsType": "import('./common/publication.v1').PublicationV1" }, "pathwayType": { - "description": "Type of the pathway pathway.", + "description": "Type of the pathway.", "type": "string", "enum": ["Normative", "Exploratory", "Predictive"] }, diff --git a/src/schema/pathwayMetadata.v2.json b/src/schema/pathwayMetadata.v2.json new file mode 100644 index 00000000..68c05f6c --- /dev/null +++ b/src/schema/pathwayMetadata.v2.json @@ -0,0 +1,688 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", + "title": "Pathway Metadata", + "description": "A schema for the pathway metadata dataset in TPR.", + "type": "object", + "properties": { + "$schema": { + "description": "URI of the schema that validates this document (see https://json-schema.org/).", + "type": "string", + "maxLength": 1000 + }, + "id": { + "description": "The unique identifier for a pathway.", + "type": "string", + "maxLength": 100 + }, + "name": { + "description": "Name of the pathway.", + "$ref": "http://pathways.rmi.org/schema/common/label.v1.json", + "tsType": "import('./common/label.v1').LabelV1" + }, + "description": { + "description": "Brief description of the pathway.", + "type": "string", + "pattern": "\\.$", + "maxLength": 100 + }, + "publication": { + "description": "Bibliographic information about the report or dataset.", + "$ref": "http://pathways.rmi.org/schema/common/publication.v1.json", + "tsType": "import('./common/publication.v1').PublicationV1" + }, + "pathwayType": { + "description": "Type of the pathway.", + "type": "string", + "enum": ["Normative", "Exploratory", "Predictive"] + }, + "modelYearNetzero": { + "description": "Year by which net zero is reached in the pathway. If Pathway does not reach net zero, this field should be omitted.", + "type": "integer", + "minimum": 2030, + "maximum": 2100 + }, + "modelYearStart": { + "description": "Year from which the model starts.", + "type": "integer", + "minimum": 1900, + "maximum": 2030 + }, + "modelYearEnd": { + "description": "Year in which the model ends.", + "type": "integer", + "minimum": 2030, + "maximum": 2100 + }, + "modelTempIncrease": { + "description": "Modeled temperature increase expected by the pathway (in degrees Celsius).", + "type": "number", + "multipleOf": 0.1, + "minimum": 0.5, + "maximum": 3 + }, + "geography": { + "description": "Geographical areas that the pathway covers.", + "$ref": "http://pathways.rmi.org/schema/common/geography.v1.json", + "tsType": "import('./common/geography.v1').GeographyV1" + }, + "sectors": { + "description": "Sectors that the pathway covers.", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "$ref": "http://pathways.rmi.org/schema/common/sector.v1.json#/$defs/displayName" + }, + "technologies": { + "type": "array", + "description": "Technologies applicable to this sector.", + "items": { + "$ref": "http://pathways.rmi.org/schema/common/technology.v1.json#/$defs/displayName" + } + } + }, + "required": ["name", "technologies"], + "additionalProperties": false + } + }, + "pathwayDescription": { + "description": "Narrative description of the pathway. Replaces v1's expertOverview: in the v1 corpus this is the '#### Pathway Description' section of it. v1's separate pathwayOverview field is retired without replacement, not merged in here -- the two texts restate each other, so merging them read as immediate self-repetition. null means no description is available.", + "type": ["string", "null"], + "pattern": "\\.$", + "maxLength": 2500 + }, + "metric": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "http://pathways.rmi.org/schema/common/metric.v1.json#/$defs/displayName", + "tsType": "import('./common/metric.v1').MetricV1['displayName']" + } + }, + "keyFeatures": { + "description": "Key features of the pathway. Every field is an array of {sector, geography, value} entries (#858), so a pathway can hold different values for different parts of its coverage. A non-varying feature carries exactly one entry at the widest applicable scope: sector 'cross-sector' for a multi-sector pathway else its lone sector, and geography 'Global' else the pathway's widest declared region or country. An entry that is absent at some scope means the resolver keeps broadening until it finds one; an explicit \"No information\" value is a real authored value that terminates that fallback chain and displays at its own scope. An empty array means nothing is authored at any scope.", + "type": "object", + "properties": { + "emissionsTrajectory": { + "description": "Describes the overall trend of greenhouse gas emissions over time, from continued growth to rapid decline. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "string", + "enum": [ + "No information", + "Significant increase", + "Moderate increase", + "Minor increase", + "Low or no change", + "Minor decrease", + "Moderate decrease", + "Significant decrease" + ] + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "energyEfficiency": { + "description": "Indicates how efficiently energy is used to produce economic output across the sectors covered in the pathway. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "string", + "enum": [ + "No information", + "Significant deterioration", + "Moderate deterioration", + "Minor deterioration", + "Low or no change", + "Minor improvement", + "Moderate improvement", + "Significant improvement" + ] + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "energyDemand": { + "description": "Captures the change in total energy consumption, driven by factors such as socio-economic development, technology shifts and consumer behavior. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "string", + "enum": [ + "No information", + "Significant decrease", + "Moderate decrease", + "Minor decrease", + "Low or no change", + "Minor increase", + "Moderate increase", + "Significant increase" + ] + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "electrification": { + "description": "Represents the extent to which energy end-uses transition from fossil fuels to electricity. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "string", + "enum": [ + "No information", + "Significant decrease", + "Moderate decrease", + "Minor decrease", + "Low or no change", + "Minor increase", + "Moderate increase", + "Significant increase" + ] + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "policyTypes": { + "description": "Identifies the types of policies modeled as drivers of the pathway, such as carbon pricing, subsidies, or mandated phaseouts of specific technologies. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "No information", + "Carbon price", + "Feed-in tariffs", + "Performance standards", + "Phaseout dates", + "Subsidies", + "Target technology shares", + "Other", + "None" + ] + } + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "technologyCostTrend": { + "description": "Describes how technology costs evolve over time, from static cost assumptions to rapidly declining costs (e.g., via learning curves). Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "string", + "enum": [ + "No information", + "Increase", + "Low or no change", + "Decrease" + ] + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "emissionsScope": { + "description": "Defines which greenhouse gases are covered in the pathway's modeled emissions. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "$ref": "http://pathways.rmi.org/schema/common/emissionsScope.v1.json", + "tsType": "import('./common/emissionsScope.v1').EmissionsScopeV1" + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "policyAmbition": { + "description": "Represents the overall stringency and intent of modeled policies relative to climate targets, often reflecting if and how far the included policies go beyond currently legislated ones. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "string", + "enum": [ + "No information", + "No policies included", + "Current/legislated policies", + "Current and drafted policies", + "NDCs, unconditional only", + "NDCs incl. conditional targets", + "High ambition policies", + "Other policy ambition" + ] + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "technologyCostsDetail": { + "description": "Specifies the level of granularity in cost data, such as total system costs or detailed CAPEX/OPEX breakdowns. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "string", + "enum": [ + "No information", + "Total costs", + "Capital costs, O&M, etc.", + "Other cost breakdown" + ] + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "newTechnologiesIncluded": { + "description": "Lists emerging or breakthrough technologies that are explicitly modeled within the pathway. These are considered in technology deployment too. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "No information", + "No new technologies", + "CCUS", + "DAC", + "Green H2/ammonia", + "SAF", + "Battery storage", + "EGS/AGS", + "Other new technologies" + ] + } + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + }, + "investmentNeeds": { + "description": "Summarizes how investment requirements are quantified, from total system to sector-level or supply-chain detail. Scoped: see keyFeatures.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "sector": { + "$ref": "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + "tsType": "import('./common/scopeSector.v2').ScopeSectorV2" + }, + "geography": { + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "value": { + "type": "string", + "enum": [ + "No information", + "Total investment", + "By sector", + "By sector, part of value chain", + "By technology", + "By tech, part of value chain" + ] + } + }, + "required": ["sector", "geography", "value"], + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "required": [ + "emissionsTrajectory", + "energyEfficiency", + "energyDemand", + "electrification", + "policyTypes", + "technologyCostTrend", + "emissionsScope", + "policyAmbition", + "technologyCostsDetail", + "newTechnologiesIncluded", + "investmentNeeds" + ] + }, + "coreDrivers": { + "description": "The drivers that shape this pathway's outcomes. Every field is required but nullable: null means the driver is not a core driver for this pathway, as distinct from a driver that is present but undescribed.", + "type": "object", + "properties": { + "policies": { + "description": "Policies modeled as a driver of this pathway.", + "type": ["string", "null"], + "pattern": "\\.$", + "maxLength": 500 + }, + "emissionsTargets": { + "description": "Emissions targets or constraints driving this pathway.", + "type": ["string", "null"], + "pattern": "\\.$", + "maxLength": 500 + }, + "technologyCosts": { + "description": "Technology cost assumptions driving this pathway.", + "type": ["string", "null"], + "pattern": "\\.$", + "maxLength": 500 + }, + "investmentChange": { + "description": "Changes in investment driving this pathway.", + "type": ["string", "null"], + "pattern": "\\.$", + "maxLength": 500 + }, + "macroeconomicDrivers": { + "description": "Macroeconomic assumptions driving this pathway.", + "type": ["string", "null"], + "pattern": "\\.$", + "maxLength": 500 + }, + "behavioralShifts": { + "description": "Behavioral or demand-side shifts driving this pathway.", + "type": ["string", "null"], + "pattern": "\\.$", + "maxLength": 500 + }, + "otherDrivers": { + "description": "Any other core driver of this pathway.", + "type": ["string", "null"], + "pattern": "\\.$", + "maxLength": 500 + } + }, + "additionalProperties": false, + "required": [ + "policies", + "emissionsTargets", + "technologyCosts", + "investmentChange", + "macroeconomicDrivers", + "behavioralShifts", + "otherDrivers" + ] + }, + "dependencies": { + "description": "Conditions the pathway's outcomes depend on. Descriptive only -- deliberately NOT part of the #869 inheritance chain, so these are not scoped by geography.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "dependency_name": { + "description": "Category of the dependency.", + "type": "string", + "enum": [ + "Policy strategy", + "Regulatory framework", + "Market and economics", + "Public acceptance", + "Consumer and client behavior", + "Infrastructure and logistics", + "Technology", + "Resource availability", + "Environmental impacts and ecosystem services", + "Labor availability" + ] + }, + "dependency_description": { + "description": "What the pathway depends on, in prose.", + "type": "string", + "pattern": "\\.$", + "maxLength": 500 + }, + "sector": { + "description": "Sector the dependency applies to. Must be one of the pathway's own declared sectors -- enforced by scripts/schema-check-files.ts, since draft-07 cannot reference sibling data.", + "$ref": "http://pathways.rmi.org/schema/common/sector.v1.json#/$defs/displayName" + }, + "evidence_type": { + "description": "Strength of the evidence for this dependency.", + "type": "string", + "enum": ["Quantitative", "Qualitative", "Anecdotal", "No evidence"] + } + }, + "required": [ + "dependency_name", + "dependency_description", + "sector", + "evidence_type" + ], + "additionalProperties": false + } + }, + "dataAvailability": { + "description": "Where and how the data behind each metric can be obtained (#870). Optional: authoring is incremental, and a pathway with no entry yet is not an invalid pathway. Absent means unknown, NOT unavailable -- `dataFormat` says unavailable.", + "type": "object", + "properties": { + "overall": { + "description": "Summary of the full timeseries file we host, plus anything that does not fit the per-metric rows. Null where there is nothing to add.", + "type": ["string", "null"], + "maxLength": 2500 + }, + "byMetric": { + "description": "One row per (metricName, sector, sectorSegment, geography). Each combination may appear only once -- enforced by scripts/schema-check-files.ts, because uniqueItems compares whole entries and so permits two rows that agree on the scope and disagree on everything else.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "object", + "properties": { + "metricName": { + "description": "Metric this row describes. Must be a metric the pathway itself reports (one of its own `metric` values) and a metric of its `sector` -- both enforced by scripts/schema-check-files.ts.", + "$ref": "http://pathways.rmi.org/schema/common/metric.v1.json#/$defs/displayName", + "tsType": "import('./common/metric.v1').MetricV1['displayName']" + }, + "sector": { + "description": "Sector this row describes. Must be one of the pathway's own declared sectors. Note this is the plain sector enum, not scopeSector.v2: there is no `cross-sector` availability, because availability is a property of a concrete dataset.", + "$ref": "http://pathways.rmi.org/schema/common/sector.v1.json#/$defs/displayName", + "tsType": "import('./common/sector.v1').SectorV1['displayName']" + }, + "sectorSegment": { + "description": "Segment within the sector. `No information` where the sector is not segmented, or where its segments are not yet defined.", + "$ref": "http://pathways.rmi.org/schema/common/sectorSegment.v1.json#/$defs/displayName", + "tsType": "import('./common/sectorSegment.v1').SectorSegmentV1['displayName']" + }, + "geography": { + "description": "Geography this row applies to. Same scope token as keyFeatures, so the table can be filtered by the detail page's geography selection (#872) using the existing scope helpers.", + "$ref": "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + "tsType": "import('./common/scopeGeography.v2').ScopeGeographyV2" + }, + "geographyCoverage": { + "description": "How geographically granular the underlying data is. A coverage class, not a scope -- `geography` above carries the scope.", + "$ref": "http://pathways.rmi.org/schema/common/dataAvailability.v1.json#/$defs/geographyCoverage" + }, + "timeResolution": { + "description": "How finely the underlying data is resolved over time.", + "$ref": "http://pathways.rmi.org/schema/common/dataAvailability.v1.json#/$defs/timeResolution" + }, + "dataFormat": { + "description": "Where the data can be obtained, and in what form. `In tool` rows link to the timeseries download.", + "$ref": "http://pathways.rmi.org/schema/common/dataAvailability.v1.json#/$defs/dataFormat" + }, + "access": { + "description": "Whether reaching the data at the publisher costs money. Must be null exactly when `dataFormat` is `In tool` -- enforced by scripts/schema-check-files.ts.", + "$ref": "http://pathways.rmi.org/schema/common/dataAvailability.v1.json#/$defs/access" + }, + "granularity": { + "description": "Dimensions the metric is broken down by, or null where it is reported as a single series. Values are technologies of this row's sector -- enforced by scripts/schema-check-files.ts, the same rule as sectors[].technologies.", + "type": ["array", "null"], + "uniqueItems": true, + "items": { + "$ref": "http://pathways.rmi.org/schema/common/technology.v1.json#/$defs/displayName", + "tsType": "import('./common/technology.v1').TechnologyV1['displayName']" + } + }, + "scopeLimitations": { + "description": "Caveats on what the data does and does not cover, in prose. Null where there are none.", + "type": ["string", "null"], + "maxLength": 500 + } + }, + "required": [ + "metricName", + "sector", + "sectorSegment", + "geography", + "geographyCoverage", + "timeResolution", + "dataFormat", + "access", + "granularity", + "scopeLimitations" + ], + "additionalProperties": false + } + } + }, + "required": ["overall", "byMetric"], + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": [ + "id", + "name", + "description", + "publication", + "pathwayType", + "geography", + "sectors", + "pathwayDescription", + "metric", + "keyFeatures", + "coreDrivers", + "dependencies" + ] +} diff --git a/src/schema/pathwayMetadata.v2.test.ts b/src/schema/pathwayMetadata.v2.test.ts new file mode 100644 index 00000000..8165dc84 --- /dev/null +++ b/src/schema/pathwayMetadata.v2.test.ts @@ -0,0 +1,347 @@ +import { describe, it, expect } from "vitest"; +import v1Json from "./pathwayMetadata.v1.json" with { type: "json" }; +import v2Json from "./pathwayMetadata.v2.json" with { type: "json" }; +import scopeSectorJson from "./common/scopeSector.v2.json" with { type: "json" }; +import sectorJson from "./common/sector.v1.json" with { type: "json" }; +import emissionsScopeJson from "./common/emissionsScope.v1.json" with { type: "json" }; + +/** + * Guards v2's keyFeatures against silent self-drift. + * + * v2 spells the scoped-entry wrapper out once per field rather than sharing a + * `$defs` entry via `allOf`. That was measured, not assumed: the `allOf` version + * validates identically and generates nicer types, but AJV's `strict: true` + * (`strictRequired`, then `strictTypes`) forces the boilerplate back in for a net + * saving of 18 lines, it cannot use `additionalProperties: false` — that keyword + * only sees its own branch's `properties`, so it would reject `value` — and the + * `propertyNames` substitute degrades the commonest authoring error from + * "must NOT have additional properties" to "property name must be valid" with the + * offending key unnamed, because `fmt()` in validateData.ts drops AJV's `params`. + * + * The cost of that choice is 11 copies of one shape, so these tests enforce what + * the `$ref` would have: that the copies stay identical, and that each field's + * `value` still matches v1's enum verbatim, which is #858's actual requirement. + */ + +/** The slice of JSON Schema draft-07 these assertions actually read. */ +interface JsonSchema { + $id?: string; + $ref?: string; + $defs?: Record; + type?: string | string[]; + enum?: string[]; + items?: JsonSchema; + properties?: Record; + required?: string[]; + additionalProperties?: boolean; + uniqueItems?: boolean; + minItems?: number; + maxLength?: number; + description?: string; + tsType?: string; +} + +const v1 = v1Json as unknown as JsonSchema; +const v2 = v2Json as unknown as JsonSchema; +const scopeSector = scopeSectorJson as unknown as JsonSchema; +const sector = sectorJson as unknown as JsonSchema; +const emissionsScope = emissionsScopeJson as unknown as JsonSchema; + +/** Throwing accessors keep every read type-safe without non-null assertions. */ +function props(schema: JsonSchema, where: string): Record { + if (!schema.properties) throw new Error(`${where}: expected properties`); + return schema.properties; +} + +function prop(schema: JsonSchema, name: string, where: string): JsonSchema { + const found = props(schema, where)[name]; + if (!found) throw new Error(`${where}: expected property ${name}`); + return found; +} + +function items(schema: JsonSchema, where: string): JsonSchema { + if (!schema.items) throw new Error(`${where}: expected items`); + return schema.items; +} + +function enumOf(schema: JsonSchema, where: string): string[] { + if (!schema.enum) throw new Error(`${where}: expected enum`); + return schema.enum; +} + +const KEY_FEATURE_FIELDS = [ + "emissionsTrajectory", + "energyEfficiency", + "energyDemand", + "electrification", + "policyTypes", + "technologyCostTrend", + "emissionsScope", + "policyAmbition", + "technologyCostsDetail", + "newTechnologiesIncluded", + "investmentNeeds", +] as const; + +/** v1 stores these as arrays, so in v2 the whole array is one entry's `value`. */ +const ARRAY_VALUED: ReadonlySet = new Set([ + "policyTypes", + "newTechnologiesIncluded", +]); + +/** v1's only field whose enum lacked "No information" — it has "None" instead. */ +const GAINED_NO_INFORMATION = "policyTypes"; + +const kf2 = prop(v2, "keyFeatures", "v2"); +const kf1 = prop(v1, "keyFeatures", "v1"); + +/** The v2 field schema (the array), and the scoped entry inside it. */ +const field = (name: string): JsonSchema => prop(kf2, name, "v2.keyFeatures"); +const entry = (name: string): JsonSchema => + items(field(name), `v2.keyFeatures.${name}`); +const entryValue = (name: string): JsonSchema => + prop(entry(name), "value", `v2.keyFeatures.${name}.items`); + +describe("pathwayMetadata.v2 keyFeatures — field set", () => { + it("declares exactly the 11 fields, and the same ones as v1", () => { + expect(Object.keys(props(kf2, "v2.keyFeatures"))).toEqual([ + ...KEY_FEATURE_FIELDS, + ]); + expect(Object.keys(props(kf2, "v2.keyFeatures"))).toEqual( + Object.keys(props(kf1, "v1.keyFeatures")), + ); + }); + + it("requires all 11 and forbids extras", () => { + expect([...(kf2.required ?? [])].sort()).toEqual( + [...KEY_FEATURE_FIELDS].sort(), + ); + expect(kf2.additionalProperties).toBe(false); + }); +}); + +describe("pathwayMetadata.v2 keyFeatures — the wrapper is identical everywhere", () => { + it.each(KEY_FEATURE_FIELDS)( + "%s is a uniqueItems array with a description", + (name) => { + const f = field(name); + expect(f.type).toBe("array"); + expect(f.uniqueItems).toBe(true); + // No minItems: an empty array is the legal "absent at every scope" state. + expect(f.minItems).toBeUndefined(); + expect(typeof f.description).toBe("string"); + expect(f.description?.endsWith(".")).toBe(true); + }, + ); + + it.each(KEY_FEATURE_FIELDS)( + "%s entries are closed {sector, geography, value}", + (name) => { + const e = entry(name); + expect(e.type).toBe("object"); + expect(Object.keys(props(e, name)).sort()).toEqual([ + "geography", + "sector", + "value", + ]); + expect([...(e.required ?? [])].sort()).toEqual([ + "geography", + "sector", + "value", + ]); + expect(e.additionalProperties).toBe(false); + }, + ); + + it("every field's sector and geography subschemas are byte-identical", () => { + // The whole point of the guard: one field drifting is the failure mode a + // shared $ref would have made impossible. + const scopes = KEY_FEATURE_FIELDS.map((name) => + JSON.stringify({ + sector: prop(entry(name), "sector", name), + geography: prop(entry(name), "geography", name), + }), + ); + expect(new Set(scopes).size).toBe(1); + }); + + it("points sector and geography at the v2 scope subschemas", () => { + const e = entry("emissionsTrajectory"); + expect(prop(e, "sector", "sector").$ref).toBe( + "http://pathways.rmi.org/schema/common/scopeSector.v2.json", + ); + expect(prop(e, "geography", "geography").$ref).toBe( + "http://pathways.rmi.org/schema/common/scopeGeography.v2.json", + ); + // Without the tsType hints the generator inlines the unions per field + // instead of importing the named types. + expect(prop(e, "sector", "sector").tsType).toContain("ScopeSectorV2"); + expect(prop(e, "geography", "geography").tsType).toContain( + "ScopeGeographyV2", + ); + }); +}); + +describe("pathwayMetadata.v2 keyFeatures — values carry over from v1", () => { + it.each(KEY_FEATURE_FIELDS)("%s value enum matches v1", (name) => { + const v2Value = entryValue(name); + const v1Value = prop(kf1, name, "v1.keyFeatures"); + + if (name === "emissionsScope") { + // A $ref in v1, so it stays a $ref — the enum lives in the common schema. + expect(v2Value.$ref).toBe(v1Value.$ref); + return; + } + + if (ARRAY_VALUED.has(name)) { + // The v1 array becomes one entry's value, not one entry per member. + expect(v2Value.type).toBe("array"); + expect(v2Value.uniqueItems).toBe(v1Value.uniqueItems); + expect(v2Value.minItems).toBe(v1Value.minItems); + const v1Members = enumOf(items(v1Value, name), name); + const expected = + name === GAINED_NO_INFORMATION + ? ["No information", ...v1Members] + : v1Members; + expect(enumOf(items(v2Value, name), name)).toEqual(expected); + return; + } + + expect(v2Value.type).toBe("string"); + expect(enumOf(v2Value, name)).toEqual(enumOf(v1Value, name)); + }); + + it('every field offers an explicit "No information" value', () => { + // #858: an explicit "No information" terminates the resolver's fallback + // chain, as distinct from an absent entry. It only works if every field can + // express it — policyTypes is the one v1 field that could not. + for (const name of KEY_FEATURE_FIELDS) { + const value = entryValue(name); + // emissionsScope holds its enum in the common subschema it $refs, so follow + // the reference rather than skipping the field — skipping would let this + // test pass while the one $ref'd field quietly lost the value. + let options: string[]; + if (value.enum) { + options = value.enum; + } else if (value.items?.enum) { + options = value.items.enum; + } else if (value.$ref === emissionsScope.$id) { + options = enumOf(emissionsScope, "emissionsScope.v1"); + } else { + throw new Error(`${name}: could not resolve a value enum`); + } + expect(options, `${name} is missing "No information"`).toContain( + "No information", + ); + } + }); +}); + +describe("scopeSector.v2 tracks sector.v1", () => { + it("is sector.v1's display names plus the cross-sector sentinel", () => { + const defs = sector.$defs; + if (!defs) throw new Error("sector.v1: expected $defs"); + const sectorNames = enumOf(defs.displayName, "sector.v1.displayName"); + const scopeNames = enumOf(scopeSector, "scopeSector.v2"); + expect(scopeNames).toContain("cross-sector"); + expect([...scopeNames].filter((n) => n !== "cross-sector").sort()).toEqual( + [...sectorNames].sort(), + ); + }); +}); + +/** + * #870's dataAvailability. The interesting properties are structural: that it is + * optional (authoring is incremental), that the row shape is closed, and that its + * vocabularies are `$ref`s rather than copies — the drift this file exists to + * catch is a vocabulary spelled out twice. + */ +describe("pathwayMetadata.v2 dataAvailability", () => { + const da = prop(v2, "dataAvailability", "v2"); + const byMetric = prop(da, "byMetric", "v2.dataAvailability"); + const row = items(byMetric, "v2.dataAvailability.byMetric"); + const rowProp = (name: string) => + prop(row, name, "v2.dataAvailability.byMetric.items"); + + it("is optional — a pathway without it is still valid", () => { + expect(v2.required ?? []).not.toContain("dataAvailability"); + }); + + it("requires both halves once present, and admits nothing else", () => { + expect([...(da.required ?? [])].sort()).toEqual(["byMetric", "overall"]); + expect(da.additionalProperties).toBe(false); + }); + + it("declares every row field as required, and admits nothing else", () => { + const expected = [ + "access", + "dataFormat", + "geography", + "geographyCoverage", + "granularity", + "metricName", + "scopeLimitations", + "sector", + "sectorSegment", + "timeResolution", + ]; + expect([...Object.keys(props(row, "row"))].sort()).toEqual(expected); + expect([...(row.required ?? [])].sort()).toEqual(expected); + expect(row.additionalProperties).toBe(false); + }); + + it("dedupes rows structurally as far as the schema can", () => { + // uniqueItems only catches wholly identical rows; two rows sharing a scope + // and differing elsewhere are caught by validateScopedEntries instead. + expect(byMetric.uniqueItems).toBe(true); + }); + + it("refs the shared vocabularies instead of restating them", () => { + const refs: Record = { + metricName: "common/metric.v1.json#/$defs/displayName", + sector: "common/sector.v1.json#/$defs/displayName", + sectorSegment: "common/sectorSegment.v1.json#/$defs/displayName", + geography: "common/scopeGeography.v2.json", + geographyCoverage: + "common/dataAvailability.v1.json#/$defs/geographyCoverage", + timeResolution: "common/dataAvailability.v1.json#/$defs/timeResolution", + dataFormat: "common/dataAvailability.v1.json#/$defs/dataFormat", + access: "common/dataAvailability.v1.json#/$defs/access", + }; + for (const [name, suffix] of Object.entries(refs)) { + expect(rowProp(name).$ref).toBe( + `http://pathways.rmi.org/schema/${suffix}`, + ); + } + }); + + it("scopes rows the same way keyFeatures does, so the same helpers apply", () => { + expect(rowProp("geography").$ref).toBe( + prop(entry("emissionsTrajectory"), "geography", "kf entry").$ref, + ); + }); + + it("uses the plain sector enum, not scopeSector — there is no cross-sector row", () => { + // Availability describes a concrete dataset, so the cross-sector sentinel + // would have nothing to resolve to. + expect(rowProp("sector").$ref).not.toContain("scopeSector"); + expect(enumOf(scopeSector, "scopeSector.v2")).toContain("cross-sector"); + }); + + it("makes granularity nullable and draws its values from technology.v1", () => { + const granularity = rowProp("granularity"); + expect(granularity.type).toEqual(["array", "null"]); + expect(granularity.uniqueItems).toBe(true); + expect(items(granularity, "granularity").$ref).toBe( + "http://pathways.rmi.org/schema/common/technology.v1.json#/$defs/displayName", + ); + }); + + it("caps the free-text fields", () => { + expect(rowProp("scopeLimitations").type).toEqual(["string", "null"]); + expect(prop(da, "overall", "v2.dataAvailability").type).toEqual([ + "string", + "null", + ]); + }); +}); diff --git a/src/types/common/dataAvailability.v1.d.ts b/src/types/common/dataAvailability.v1.d.ts new file mode 100644 index 00000000..91824795 --- /dev/null +++ b/src/types/common/dataAvailability.v1.d.ts @@ -0,0 +1,41 @@ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSON Schema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * How geographically granular the underlying data is. + */ +export type GeographyCoverage = "Global" | "Regional" | "Country"; +/** + * How finely the underlying data is resolved over time. + */ +export type TimeResolution = + | "No information" + | "Single year (2050)" + | "Medium-term" + | "10-year" + | "5-10-year" + | "5-year" + | "1-year" + | "Other"; +/** + * Where the data can be obtained, and in what form. + */ +export type DataFormat = + "In tool" | "Tabular in publication" | "Text in publication"; +/** + * Whether reaching the data at the publisher costs money. Null when we host it. + */ +export type Access = "Free" | "Paywalled" | null; + +/** + * The closed vocabularies describing how a metric's underlying data can be obtained (#870). + */ +export interface DataAvailabilityV1 { + geographyCoverage: GeographyCoverage; + timeResolution: TimeResolution; + dataFormat: DataFormat; + access: Access; +} diff --git a/src/types/common/scopeGeography.v2.d.ts b/src/types/common/scopeGeography.v2.d.ts new file mode 100644 index 00000000..23dce12f --- /dev/null +++ b/src/types/common/scopeGeography.v2.d.ts @@ -0,0 +1,10 @@ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSON Schema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeographyV2 = string; diff --git a/src/types/common/scopeSector.v2.d.ts b/src/types/common/scopeSector.v2.d.ts new file mode 100644 index 00000000..d124f829 --- /dev/null +++ b/src/types/common/scopeSector.v2.d.ts @@ -0,0 +1,26 @@ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSON Schema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSectorV2 = + | "cross-sector" + | "Land Use" + | "Agriculture" + | "Buildings" + | "Steel" + | "Cement" + | "Chemicals" + | "Coal Mining" + | "Oil (Upstream)" + | "Gas (Upstream)" + | "Power" + | "Automotive" + | "Aviation" + | "Rail" + | "Shipping" + | "Other"; diff --git a/src/types/common/sectorSegment.v1.d.ts b/src/types/common/sectorSegment.v1.d.ts new file mode 100644 index 00000000..f403eca4 --- /dev/null +++ b/src/types/common/sectorSegment.v1.d.ts @@ -0,0 +1,21 @@ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSON Schema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * Display name of the sector segment as presented in tables. + */ +export type DisplayName = + | "No information" + | "Power generation" + | "Storage" + | "Transmission & Distribution"; + +/** + * Segments within a sector + */ +export interface SectorSegmentV1 { + displayName: DisplayName; +} diff --git a/src/types/index.ts b/src/types/index.ts index b4f56054..acd17b42 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,12 +1,23 @@ import type { FacetMode } from "../utils/searchUtils"; import type { PathwayMetadataV1 } from "./pathwayMetadata.v1"; +import type { PathwayMetadataV2 } from "./pathwayMetadata.v2"; import type { PublicationV1 } from "./common/publication.v1"; import type { GeographyV1 } from "./common/geography.v1"; -// Re-export the (current) versioned pathway metadata type as generic -export type PathwayMetadataType = PathwayMetadataV1; +// Re-export the (current) versioned pathway metadata type as generic. +// v2 as of #858: src/data/pathwayMetadata.ts validates against v2, so only v2 +// documents reach the app and this is the shape every consumer sees. +export type PathwayMetadataType = PathwayMetadataV2; export type PublicationType = PublicationV1; +// Both versions are exported for the migration window. v1 and v2 documents +// coexist in src/data — validateData routes each by its own $schema $id. +export type { PathwayMetadataV1, PathwayMetadataV2 }; + +/** A single scoped keyFeatures entry: {sector, geography, value} (#858). */ +export type ScopedKeyFeature = + PathwayMetadataV2["keyFeatures"][K][number]; + // Enum-like types derived from the schema export type PathwayType = PathwayMetadataType["pathwayType"]; export type Sector = PathwayMetadataType["sectors"][number]["name"]; diff --git a/src/types/pathwayMetadata.v1.d.ts b/src/types/pathwayMetadata.v1.d.ts index 873b7085..d6cc4301 100644 --- a/src/types/pathwayMetadata.v1.d.ts +++ b/src/types/pathwayMetadata.v1.d.ts @@ -41,7 +41,7 @@ export interface PathwayMetadataV1 { description: string; publication: Publication; /** - * Type of the pathway pathway. + * Type of the pathway. */ pathwayType: "Normative" | "Exploratory" | "Predictive"; /** diff --git a/src/types/pathwayMetadata.v2.d.ts b/src/types/pathwayMetadata.v2.d.ts new file mode 100644 index 00000000..f26ced1b --- /dev/null +++ b/src/types/pathwayMetadata.v2.d.ts @@ -0,0 +1,567 @@ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSON Schema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +/** + * Name of the pathway. + */ +export type Label = import("./common/label.v1").LabelV1; +/** + * Bibliographic information about the report or dataset. + */ +export type Publication = import("./common/publication.v1").PublicationV1; +/** + * Geographical areas that the pathway covers. + */ +export type Geography = import("./common/geography.v1").GeographyV1; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector1 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography1 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector2 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography2 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector3 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography3 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector4 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography4 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector5 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography5 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector6 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography6 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * Defines which greenhouse gases are covered in the pathway's modeled emissions. + */ +export type EmissionsScope = + import("./common/emissionsScope.v1").EmissionsScopeV1; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector7 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography7 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector8 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography8 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector9 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography9 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * The sector axis of a scoped keyFeatures entry: one of the sector display names, or the widest sentinel 'cross-sector'. + */ +export type ScopeSector10 = import("./common/scopeSector.v2").ScopeSectorV2; +/** + * The geography axis of a scoped keyFeatures entry: 'Global', 'cross-region', an ISO-3166-1 alpha-2 country code, or an author-defined region label. + */ +export type ScopeGeography10 = + import("./common/scopeGeography.v2").ScopeGeographyV2; +/** + * Geography this row applies to. Same scope token as keyFeatures, so the table can be filtered by the detail page's geography selection (#872) using the existing scope helpers. + */ +export type ScopeGeography11 = + import("./common/scopeGeography.v2").ScopeGeographyV2; + +/** + * A schema for the pathway metadata dataset in TPR. + */ +export interface PathwayMetadataV2 { + /** + * URI of the schema that validates this document (see https://json-schema.org/). + */ + $schema?: string; + /** + * The unique identifier for a pathway. + */ + id: string; + name: Label; + /** + * Brief description of the pathway. + */ + description: string; + publication: Publication; + /** + * Type of the pathway. + */ + pathwayType: "Normative" | "Exploratory" | "Predictive"; + /** + * Year by which net zero is reached in the pathway. If Pathway does not reach net zero, this field should be omitted. + */ + modelYearNetzero?: number; + /** + * Year from which the model starts. + */ + modelYearStart?: number; + /** + * Year in which the model ends. + */ + modelYearEnd?: number; + /** + * Modeled temperature increase expected by the pathway (in degrees Celsius). + */ + modelTempIncrease?: number; + geography: Geography; + /** + * Sectors that the pathway covers. + */ + sectors: { + /** + * Display name of a sector. + */ + name: + | "Land Use" + | "Agriculture" + | "Buildings" + | "Steel" + | "Cement" + | "Chemicals" + | "Coal Mining" + | "Oil (Upstream)" + | "Gas (Upstream)" + | "Power" + | "Automotive" + | "Aviation" + | "Rail" + | "Shipping" + | "Other"; + /** + * Technologies applicable to this sector. + */ + technologies: ( + | "Precision Agriculture" + | "Agroforestry" + | "Bioenergy Crops" + | "Energy Efficiency" + | "Smart Grids" + | "Renewable Heating" + | "Heat Pumps" + | "Building Automation" + | "Smart Appliances" + | "Insulation" + | "Carbon Capture and Storage" + | "Electrification" + | "Process Optimization" + | "Hydrogen Use" + | "Coal" + | "Oil" + | "Gas" + | "Wind" + | "Solar" + | "Nuclear" + | "Biomass" + | "Hydro" + | "Renewables" + | "Electric Vehicles" + | "Hydrogen Vehicles" + | "Biofuels" + | "Public Transport" + | "Active Mobility" + | "Aviation Efficiency" + | "Maritime Efficiency" + | "Other" + )[]; + }[]; + /** + * Narrative description of the pathway. Replaces v1's expertOverview: in the v1 corpus this is the '#### Pathway Description' section of it. v1's separate pathwayOverview field is retired without replacement, not merged in here -- the two texts restate each other, so merging them read as immediate self-repetition. null means no description is available. + */ + pathwayDescription: string | null; + metric: import("./common/metric.v1").MetricV1["displayName"][]; + /** + * Key features of the pathway. Every field is an array of {sector, geography, value} entries (#858), so a pathway can hold different values for different parts of its coverage. A non-varying feature carries exactly one entry at the widest applicable scope: sector 'cross-sector' for a multi-sector pathway else its lone sector, and geography 'Global' else the pathway's widest declared region or country. An entry that is absent at some scope means the resolver keeps broadening until it finds one; an explicit "No information" value is a real authored value that terminates that fallback chain and displays at its own scope. An empty array means nothing is authored at any scope. + */ + keyFeatures: { + /** + * Describes the overall trend of greenhouse gas emissions over time, from continued growth to rapid decline. Scoped: see keyFeatures. + */ + emissionsTrajectory: { + sector: ScopeSector; + geography: ScopeGeography; + value: + | "No information" + | "Significant increase" + | "Moderate increase" + | "Minor increase" + | "Low or no change" + | "Minor decrease" + | "Moderate decrease" + | "Significant decrease"; + }[]; + /** + * Indicates how efficiently energy is used to produce economic output across the sectors covered in the pathway. Scoped: see keyFeatures. + */ + energyEfficiency: { + sector: ScopeSector1; + geography: ScopeGeography1; + value: + | "No information" + | "Significant deterioration" + | "Moderate deterioration" + | "Minor deterioration" + | "Low or no change" + | "Minor improvement" + | "Moderate improvement" + | "Significant improvement"; + }[]; + /** + * Captures the change in total energy consumption, driven by factors such as socio-economic development, technology shifts and consumer behavior. Scoped: see keyFeatures. + */ + energyDemand: { + sector: ScopeSector2; + geography: ScopeGeography2; + value: + | "No information" + | "Significant decrease" + | "Moderate decrease" + | "Minor decrease" + | "Low or no change" + | "Minor increase" + | "Moderate increase" + | "Significant increase"; + }[]; + /** + * Represents the extent to which energy end-uses transition from fossil fuels to electricity. Scoped: see keyFeatures. + */ + electrification: { + sector: ScopeSector3; + geography: ScopeGeography3; + value: + | "No information" + | "Significant decrease" + | "Moderate decrease" + | "Minor decrease" + | "Low or no change" + | "Minor increase" + | "Moderate increase" + | "Significant increase"; + }[]; + /** + * Identifies the types of policies modeled as drivers of the pathway, such as carbon pricing, subsidies, or mandated phaseouts of specific technologies. Scoped: see keyFeatures. + */ + policyTypes: { + sector: ScopeSector4; + geography: ScopeGeography4; + /** + * @minItems 1 + */ + value: [ + ( + | "No information" + | "Carbon price" + | "Feed-in tariffs" + | "Performance standards" + | "Phaseout dates" + | "Subsidies" + | "Target technology shares" + | "Other" + | "None" + ), + ...( + | "No information" + | "Carbon price" + | "Feed-in tariffs" + | "Performance standards" + | "Phaseout dates" + | "Subsidies" + | "Target technology shares" + | "Other" + | "None" + )[], + ]; + }[]; + /** + * Describes how technology costs evolve over time, from static cost assumptions to rapidly declining costs (e.g., via learning curves). Scoped: see keyFeatures. + */ + technologyCostTrend: { + sector: ScopeSector5; + geography: ScopeGeography5; + value: "No information" | "Increase" | "Low or no change" | "Decrease"; + }[]; + /** + * Defines which greenhouse gases are covered in the pathway's modeled emissions. Scoped: see keyFeatures. + */ + emissionsScope: { + sector: ScopeSector6; + geography: ScopeGeography6; + value: EmissionsScope; + }[]; + /** + * Represents the overall stringency and intent of modeled policies relative to climate targets, often reflecting if and how far the included policies go beyond currently legislated ones. Scoped: see keyFeatures. + */ + policyAmbition: { + sector: ScopeSector7; + geography: ScopeGeography7; + value: + | "No information" + | "No policies included" + | "Current/legislated policies" + | "Current and drafted policies" + | "NDCs, unconditional only" + | "NDCs incl. conditional targets" + | "High ambition policies" + | "Other policy ambition"; + }[]; + /** + * Specifies the level of granularity in cost data, such as total system costs or detailed CAPEX/OPEX breakdowns. Scoped: see keyFeatures. + */ + technologyCostsDetail: { + sector: ScopeSector8; + geography: ScopeGeography8; + value: + | "No information" + | "Total costs" + | "Capital costs, O&M, etc." + | "Other cost breakdown"; + }[]; + /** + * Lists emerging or breakthrough technologies that are explicitly modeled within the pathway. These are considered in technology deployment too. Scoped: see keyFeatures. + */ + newTechnologiesIncluded: { + sector: ScopeSector9; + geography: ScopeGeography9; + /** + * @minItems 1 + */ + value: [ + ( + | "No information" + | "No new technologies" + | "CCUS" + | "DAC" + | "Green H2/ammonia" + | "SAF" + | "Battery storage" + | "EGS/AGS" + | "Other new technologies" + ), + ...( + | "No information" + | "No new technologies" + | "CCUS" + | "DAC" + | "Green H2/ammonia" + | "SAF" + | "Battery storage" + | "EGS/AGS" + | "Other new technologies" + )[], + ]; + }[]; + /** + * Summarizes how investment requirements are quantified, from total system to sector-level or supply-chain detail. Scoped: see keyFeatures. + */ + investmentNeeds: { + sector: ScopeSector10; + geography: ScopeGeography10; + value: + | "No information" + | "Total investment" + | "By sector" + | "By sector, part of value chain" + | "By technology" + | "By tech, part of value chain"; + }[]; + }; + /** + * The drivers that shape this pathway's outcomes. Every field is required but nullable: null means the driver is not a core driver for this pathway, as distinct from a driver that is present but undescribed. + */ + coreDrivers: { + /** + * Policies modeled as a driver of this pathway. + */ + policies: string | null; + /** + * Emissions targets or constraints driving this pathway. + */ + emissionsTargets: string | null; + /** + * Technology cost assumptions driving this pathway. + */ + technologyCosts: string | null; + /** + * Changes in investment driving this pathway. + */ + investmentChange: string | null; + /** + * Macroeconomic assumptions driving this pathway. + */ + macroeconomicDrivers: string | null; + /** + * Behavioral or demand-side shifts driving this pathway. + */ + behavioralShifts: string | null; + /** + * Any other core driver of this pathway. + */ + otherDrivers: string | null; + }; + /** + * Conditions the pathway's outcomes depend on. Descriptive only -- deliberately NOT part of the #869 inheritance chain, so these are not scoped by geography. + */ + dependencies: { + /** + * Category of the dependency. + */ + dependency_name: + | "Policy strategy" + | "Regulatory framework" + | "Market and economics" + | "Public acceptance" + | "Consumer and client behavior" + | "Infrastructure and logistics" + | "Technology" + | "Resource availability" + | "Environmental impacts and ecosystem services" + | "Labor availability"; + /** + * What the pathway depends on, in prose. + */ + dependency_description: string; + /** + * Display name of a sector. + */ + sector: + | "Land Use" + | "Agriculture" + | "Buildings" + | "Steel" + | "Cement" + | "Chemicals" + | "Coal Mining" + | "Oil (Upstream)" + | "Gas (Upstream)" + | "Power" + | "Automotive" + | "Aviation" + | "Rail" + | "Shipping" + | "Other"; + /** + * Strength of the evidence for this dependency. + */ + evidence_type: "Quantitative" | "Qualitative" | "Anecdotal" | "No evidence"; + }[]; + /** + * Where and how the data behind each metric can be obtained (#870). Optional: authoring is incremental, and a pathway with no entry yet is not an invalid pathway. Absent means unknown, NOT unavailable -- `dataFormat` says unavailable. + */ + dataAvailability?: { + /** + * Summary of the full timeseries file we host, plus anything that does not fit the per-metric rows. Null where there is nothing to add. + */ + overall: string | null; + /** + * One row per (metricName, sector, sectorSegment, geography). Each combination may appear only once -- enforced by scripts/schema-check-files.ts, because uniqueItems compares whole entries and so permits two rows that agree on the scope and disagree on everything else. + */ + byMetric: { + /** + * Metric this row describes. Must be a metric the pathway itself reports (one of its own `metric` values) and a metric of its `sector` -- both enforced by scripts/schema-check-files.ts. + */ + metricName: import("./common/metric.v1").MetricV1["displayName"]; + /** + * Display name of a sector. + */ + sector: import("./common/sector.v1").SectorV1["displayName"]; + /** + * Segment within the sector. `No information` where the sector is not segmented, or where its segments are not yet defined. + */ + sectorSegment: import("./common/sectorSegment.v1").SectorSegmentV1["displayName"]; + geography: ScopeGeography11; + /** + * How geographically granular the underlying data is. A coverage class, not a scope -- `geography` above carries the scope. + */ + geographyCoverage: "Global" | "Regional" | "Country"; + /** + * How finely the underlying data is resolved over time. + */ + timeResolution: + | "No information" + | "Single year (2050)" + | "Medium-term" + | "10-year" + | "5-10-year" + | "5-year" + | "1-year" + | "Other"; + /** + * Where the data can be obtained, and in what form. `In tool` rows link to the timeseries download. + */ + dataFormat: "In tool" | "Tabular in publication" | "Text in publication"; + /** + * Whether reaching the data at the publisher costs money. Must be null exactly when `dataFormat` is `In tool` -- enforced by scripts/schema-check-files.ts. + */ + access: "Free" | "Paywalled" | null; + /** + * Dimensions the metric is broken down by, or null where it is reported as a single series. Values are technologies of this row's sector -- enforced by scripts/schema-check-files.ts, the same rule as sectors[].technologies. + */ + granularity: + import("./common/technology.v1").TechnologyV1["displayName"][] | null; + /** + * Caveats on what the data does and does not cover, in prose. Null where there are none. + */ + scopeLimitations: string | null; + }[]; + }; +} diff --git a/src/utils/keyFeatureScope.test.ts b/src/utils/keyFeatureScope.test.ts new file mode 100644 index 00000000..2782e1ca --- /dev/null +++ b/src/utils/keyFeatureScope.test.ts @@ -0,0 +1,367 @@ +import { describe, it, expect } from "vitest"; +import { + entryValues, + sectorScopeContains, + entryISOSet, + geographyScopeOverlaps, + entriesInScope, + valuesInScope, + widestValue, +} from "./keyFeatureScope"; +import { ABSENT_FILTER_TOKEN } from "./absent"; +import type { PathwayMetadataType } from "../types"; + +/** + * A pathway covering Power and Steel, with one mapped region, one unmapped region + * (the NGFS shape pending #801), and one standalone country. + */ +function pathway(over: Partial = {}): PathwayMetadataType { + return { + sectors: [ + { name: "Power", technologies: [] }, + { name: "Steel", technologies: [] }, + ], + geography: { + regions: { + "South East Asia": ["ID", "TH", "VN"], + "Unmapped Region": [], + }, + country: ["US"], + }, + ...over, + } as unknown as PathwayMetadataType; +} + +const e = (sector: string, geography: string, value: string | string[]) => ({ + sector, + geography, + value, +}); + +describe("entryValues", () => { + it("flattens scalar and array-valued entries alike", () => { + expect( + entryValues([ + e("Power", "Global", "A"), + e("Steel", "Global", ["B", "C"]), + ]), + ).toEqual(["A", "B", "C"]); + }); + + it("returns [] for an empty entry list", () => { + expect(entryValues([])).toEqual([]); + }); + + it("returns [] for a non-array — a stale v1 scalar degrades, not throws", () => { + // Relevant while v1 and v2 coexist: a v1-shaped scalar must not blow up here. + expect(entryValues("Significant decrease")).toEqual([]); + expect(entryValues(null)).toEqual([]); + expect(entryValues(undefined)).toEqual([]); + }); +}); + +describe("sectorScopeContains", () => { + const declared = ["Power", "Steel"]; + + it("matches an exact sector", () => { + expect(sectorScopeContains("Power", "Power", declared)).toBe(true); + }); + + it("does not match a different sector", () => { + expect(sectorScopeContains("Power", "Steel", declared)).toBe(false); + }); + + it("cross-sector contains any sector the pathway declares", () => { + expect(sectorScopeContains("cross-sector", "Steel", declared)).toBe(true); + }); + + it("cross-sector is NOT a universal match", () => { + // Per Jacob on #869: cross-sector is the union of the pathway's own sectors, + // so a pathway covering only Power and Steel does not answer a Cement query. + expect(sectorScopeContains("cross-sector", "Cement", declared)).toBe(false); + }); +}); + +describe("entryISOSet", () => { + it("returns null for Global, meaning everything", () => { + expect(entryISOSet("Global", pathway())).toBeNull(); + }); + + it("resolves a region label through the pathway's own mapping", () => { + expect( + [...(entryISOSet("South East Asia", pathway()) ?? [])].sort(), + ).toEqual(["ID", "TH", "VN"]); + }); + + it("resolves a bare country code", () => { + expect([...(entryISOSet("US", pathway()) ?? [])]).toEqual(["US"]); + }); + + it("resolves an unmapped region to the empty set, not to everything", () => { + // #801: a region the publication never mapped must match nothing rather than + // silently behaving like Global. + expect(entryISOSet("Unmapped Region", pathway())?.size).toBe(0); + }); + + it("resolves cross-region to the pathway's whole ISO coverage", () => { + const set = entryISOSet("cross-region", pathway()); + expect([...(set ?? [])].sort()).toEqual(["ID", "TH", "US", "VN"]); + }); + + it("resolves an unrecognised label to the empty set", () => { + expect(entryISOSet("Souteast Asia", pathway())?.size).toBe(0); + }); +}); + +describe("geographyScopeOverlaps", () => { + const p = pathway(); + + it("a Global entry answers any query", () => { + expect(geographyScopeOverlaps("Global", "TH", p)).toBe(true); + }); + + it("a region entry answers a country inside it", () => { + expect(geographyScopeOverlaps("South East Asia", "TH", p)).toBe(true); + }); + + it("a country entry answers a region query that includes it", () => { + // Overlap, not containment. "Southeast Asia" is the query vocabulary's + // label (11 ISO codes) and includes TH, so a Thailand-scoped entry is a + // match. Under the old containment rule this was false, which is what made + // region filters drop pathways whose own region list differs by a country. + expect(geographyScopeOverlaps("TH", "Southeast Asia", p)).toBe(true); + }); + + it("tolerates a publication label the query vocabulary does not share", () => { + // The pathway's own regions are named "South East Asia" (with spaces); the + // filter vocabulary calls it "Southeast Asia". The two lists also differ by + // one country (TL), so containment would fail even once spelling matched. + // Overlap makes the intersection sufficient, which is the point of #783. + expect(geographyScopeOverlaps("South East Asia", "Southeast Asia", p)).toBe( + true, + ); + }); + + it("does not match a country outside the entry's region", () => { + expect(geographyScopeOverlaps("South East Asia", "US", p)).toBe(false); + }); + + it("only a Global entry answers a Global query", () => { + // Mirrors the geography facet's `wantGlobal && pGlobal`: selecting "Global" + // means whole-world pathways, not "match anything". + expect(geographyScopeOverlaps("Global", "Global", p)).toBe(true); + expect(geographyScopeOverlaps("South East Asia", "Global", p)).toBe(false); + }); + + it("an unrecognised token matches nothing", () => { + // "South East Asia" is a publication label, not a query-vocabulary token, so + // it resolves to an empty ISO set and must not vacuously match. + expect(geographyScopeOverlaps("TH", "South East Asia", p)).toBe(false); + expect(geographyScopeOverlaps("TH", "Souteast Asia", p)).toBe(false); + }); + + it("the absent bucket does not constrain which scope to read", () => { + expect( + geographyScopeOverlaps("South East Asia", ABSENT_FILTER_TOKEN, p), + ).toBe(true); + }); + + it("an unmapped region overlaps nothing", () => { + expect(geographyScopeOverlaps("Unmapped Region", "TH", p)).toBe(false); + }); +}); + +describe("entriesInScope", () => { + const entries = [ + e("cross-sector", "Global", "wide"), + e("Power", "South East Asia", "power-sea"), + e("Steel", "US", "steel-us"), + ]; + const p = pathway(); + + it("returns every entry when neither axis is filtered", () => { + expect(entriesInScope(entries, {}, p)).toHaveLength(3); + }); + + it("keeps entries whose sector contains the queried sector", () => { + expect( + entriesInScope(entries, { sectors: ["Power"] }, p).map((x) => x.value), + ).toEqual(["wide", "power-sea"]); + }); + + it("keeps entries whose geography contains the queried country", () => { + expect( + entriesInScope(entries, { geographies: ["TH"] }, p).map((x) => x.value), + ).toEqual(["wide", "power-sea"]); + }); + + it("applies both axes together", () => { + expect( + entriesInScope( + entries, + { sectors: ["Steel"], geographies: ["US"] }, + p, + ).map((x) => x.value), + ).toEqual(["wide", "steel-us"]); + }); + + it("excludes an entry when only one axis matches", () => { + // Steel data exists, but only for the US — not for Thailand. + expect( + entriesInScope( + entries, + { sectors: ["Steel"], geographies: ["TH"] }, + p, + ).map((x) => x.value), + ).toEqual(["wide"]); + }); + + it("treats several selections on one axis as 'any of them'", () => { + expect( + entriesInScope(entries, { sectors: ["Power", "Steel"] }, p).map( + (x) => x.value, + ), + ).toEqual(["wide", "power-sea", "steel-us"]); + }); + + it("can narrow to nothing when no entry covers the query", () => { + const narrow = [e("Power", "South East Asia", "power-sea")]; + expect(entriesInScope(narrow, { geographies: ["US"] }, p)).toEqual([]); + }); + + it("returns [] for absent entries", () => { + expect(entriesInScope(undefined, { sectors: ["Power"] }, p)).toEqual([]); + expect(entriesInScope([], { sectors: ["Power"] }, p)).toEqual([]); + }); +}); + +describe("entriesInScope — the ABSENT/None token", () => { + // Regression: the None bucket is a predicate about the pathway ("has no + // sectors"), not a scope. geographyScopeContains always ignored it, but the + // sector axis compared it as a sector name, so Sector=None plus a keyFeature + // facet filtered out every entry and the pathway looked empty. + const entries = [ + e("cross-sector", "Global", "wide"), + e("Power", "South East Asia", "power-sea"), + ]; + const p = pathway(); + + it("does not constrain the sector axis", () => { + expect( + entriesInScope(entries, { sectors: [ABSENT_FILTER_TOKEN] }, p).map( + (x) => x.value, + ), + ).toEqual(["wide", "power-sea"]); + }); + + it("does not constrain the geography axis", () => { + expect( + entriesInScope(entries, { geographies: [ABSENT_FILTER_TOKEN] }, p).map( + (x) => x.value, + ), + ).toEqual(["wide", "power-sea"]); + }); + + it("is ignored on both axes at once", () => { + expect( + entriesInScope( + entries, + { sectors: [ABSENT_FILTER_TOKEN], geographies: [ABSENT_FILTER_TOKEN] }, + p, + ), + ).toHaveLength(2); + }); + + it("still applies the concrete tokens alongside it", () => { + // None + Power: the real sector still narrows; None adds no constraint. + expect( + entriesInScope( + entries, + { sectors: [ABSENT_FILTER_TOKEN, "Steel"] }, + p, + ).map((x) => x.value), + ).toEqual(["wide"]); + }); + + it("treats it the same for a pathway that declares no sectors at all", () => { + const noSectors = pathway({ sectors: [] }); + expect( + entriesInScope( + [e("cross-sector", "Global", "wide")], + { sectors: [ABSENT_FILTER_TOKEN] }, + noSectors, + ), + ).toHaveLength(1); + }); +}); + +describe("widestValue", () => { + it("returns undefined when nothing is authored", () => { + expect(widestValue([])).toBeUndefined(); + expect(widestValue(undefined)).toBeUndefined(); + }); + + it("returns the only value when there is one entry", () => { + expect(widestValue([e("cross-sector", "Global", "only")])).toBe("only"); + }); + + it("prefers cross-sector over a named sector", () => { + expect( + widestValue([ + e("Power", "Global", "narrow"), + e("cross-sector", "Global", "wide"), + ]), + ).toBe("wide"); + }); + + it("prefers Global over a region, and a region over a country", () => { + expect( + widestValue([ + e("cross-sector", "TH", "country"), + e("cross-sector", "South East Asia", "region"), + e("cross-sector", "Global", "global"), + ]), + ).toBe("global"); + expect( + widestValue([ + e("cross-sector", "TH", "country"), + e("cross-sector", "South East Asia", "region"), + ]), + ).toBe("region"); + }); + + it("ranks sector ahead of geography", () => { + // A cross-sector entry wins even when its geography is narrower, matching the + // "sector > geography" precedence #869 defines for its cost model. + expect( + widestValue([ + e("Power", "Global", "power-global"), + e("cross-sector", "TH", "cross-th"), + ]), + ).toBe("cross-th"); + }); + + it("preserves an array value intact", () => { + expect(widestValue([e("cross-sector", "Global", ["a", "b"])])).toEqual([ + "a", + "b", + ]); + }); + + it("degrades to undefined for a stale v1 scalar", () => { + expect(widestValue("Moderate decrease")).toBeUndefined(); + }); +}); + +describe("valuesInScope", () => { + it("flattens the in-scope entries' values", () => { + const entries = [ + e("cross-sector", "Global", ["a", "b"]), + e("Steel", "US", "c"), + ]; + expect(valuesInScope(entries, { sectors: ["Power"] }, pathway())).toEqual([ + "a", + "b", + ]); + }); +}); diff --git a/src/utils/keyFeatureScope.ts b/src/utils/keyFeatureScope.ts new file mode 100644 index 00000000..766dbe5f --- /dev/null +++ b/src/utils/keyFeatureScope.ts @@ -0,0 +1,243 @@ +/** + * Reading v2's scoped keyFeatures entries (#858). + * + * Each keyFeature is an array of `{sector, geography, value}` entries, so + * "what is this pathway's emissionsTrajectory?" now depends on which part of the + * pathway's coverage you are asking about. This module answers two questions the + * search layer needs: + * + * - which entries are relevant to the user's current sector/geography filter + * ({@link entriesInScope}), and + * - what values those entries carry ({@link entryValues}). + * + * Deliberately **inclusion only**: an entry is relevant when its scope meets the + * query on both axes — sector by containment, geography by non-empty ISO + * intersection (see geographyScopeOverlaps for why the two axes differ). There is + * no cost model, no ranking, and no notion of how far the query had to broaden — + * that is #869's resolver, which is where strict containment belongs, as the basis + * for ranking rather than as a hard filter. + */ +import type { GeographyCode, PathwayMetadataType } from "../types"; +import { pathwayISOCoverage, toISO2 } from "./geographyUtils"; +import { selectedGeographyToISO } from "./filterRegions"; +import { ABSENT_FILTER_TOKEN } from "./absent"; + +/** Sector sentinel: the union of *this pathway's own* declared sectors. */ +export const CROSS_SECTOR = "cross-sector"; +/** Geography sentinels: everything, and a multi-region non-global aggregate. */ +export const GLOBAL_SCOPE = "Global"; +export const CROSS_REGION = "cross-region"; + +/** One scoped entry, structurally — the 11 fields differ only in `value`. */ +export interface ScopedEntry { + sector: string; + geography: string; + value: string | string[]; +} + +/** + * Coerce a field's value to entries, tolerating anything that is not an array. + * + * Takes `unknown` on purpose. While v1 and v2 coexist a v1-shaped scalar can + * reach these helpers from a hand-built fixture or a stale mock, and degrading to + * "no entries" is better than throwing. `Array.isArray` narrows a typed + * `readonly T[]` union to `any[]`, so the cast is what keeps this type-safe. + */ +function asEntries(value: unknown): readonly ScopedEntry[] { + return Array.isArray(value) ? (value as readonly ScopedEntry[]) : []; +} + +/** Flatten one field's entries to the values they carry, array-valued or not. */ +export function entryValues(entries: unknown): string[] { + return asEntries(entries).flatMap((e) => { + if (Array.isArray(e.value)) return e.value; + return e.value != null ? [e.value] : []; + }); +} + +/** + * Does an entry's sector scope contain a queried sector? + * + * `cross-sector` is **not** a universal match (per Jacob on #869): it means the + * union of the sectors this pathway declares, so a pathway covering only Y and Z + * does not answer a query for sector X even though its `cross-sector` values are + * nominally broad enough. + */ +export function sectorScopeContains( + entrySector: string, + querySector: string, + declaredSectors: readonly string[], +): boolean { + if (entrySector === querySector) return true; + if (entrySector === CROSS_SECTOR) + return declaredSectors.includes(querySector); + return false; +} + +/** + * The ISO codes an entry's geography scope covers, or `null` for "everything". + * + * A region label resolves through the pathway's own `geography.regions` mapping, + * which is why an unmapped region (empty member array, e.g. the NGFS files + * pending #801) resolves to the empty set and therefore matches nothing rather + * than matching everything. + */ +export function entryISOSet( + entryGeography: string, + pathway: PathwayMetadataType, +): Set | null { + if (entryGeography === GLOBAL_SCOPE) return null; + + const geo = pathway.geography; + if (entryGeography === CROSS_REGION) return pathwayISOCoverage(geo); + + const members = geo?.regions?.[entryGeography]; + if (Array.isArray(members)) return new Set(members); + + const iso = toISO2(entryGeography); + if (iso) return new Set([iso as GeographyCode]); + + return new Set(); +} + +/** + * Does an entry's geography scope overlap a selected geography token? + * + * **Overlap, not containment** — a non-empty intersection is a match. Requiring + * the query to be a strict subset of the entry looks tidier but is wrong here, + * because the query vocabulary and each publication's own region membership are + * different lists by design (#783), so they rarely coincide exactly. Concretely: + * the filter vocabulary's "Southeast Asia" carries 11 codes including TL, while + * ACE's own "South East Asia" carries 10 and omits it. Under containment that one + * country made every ACE pathway vanish whenever a region filter was combined + * with a keyFeature facet, even though the geography facet itself kept them -- + * it has always used overlap (`isoSets.some(overlaps)` in filterPathways). This + * now matches that behaviour, so the two cannot disagree about the same pathway. + * + * "Global" stays a distinct predicate rather than "every code", again mirroring + * the facet (`wantGlobal && pGlobal`): a Global query is answered only by a + * Global-scoped entry, so selecting it does not quietly match everything. + */ +export function geographyScopeOverlaps( + entryGeography: string, + queryToken: string, + pathway: PathwayMetadataType, +): boolean { + const query = selectedGeographyToISO(queryToken); + // The "None" bucket says nothing about which scope to read. entriesInScope now + // strips it before calling this, so the guard is only for direct callers. + if (query.kind === "absent") return true; + + const entrySet = entryISOSet(entryGeography, pathway); + if (entrySet === null) return true; // a Global entry answers anything + + if (query.kind === "global") return false; // only a Global entry answers Global + // An unrecognised token, or a region the publication never mapped, yields an + // empty set and so overlaps nothing -- a stale selection matches nothing + // rather than everything. + for (const code of query.iso) if (entrySet.has(code)) return true; + return false; +} + +export interface ScopeQuery { + /** Selected sector tokens; empty leaves the sector axis unconstrained. */ + sectors?: readonly string[]; + /** Selected geography tokens; empty leaves the geography axis unconstrained. */ + geographies?: readonly string[]; +} + +/** Selected tokens with the ABSENT/"None" bucket removed — see entriesInScope. */ +function dropAbsent(tokens: readonly string[] | undefined): readonly string[] { + if (!tokens || tokens.length === 0) return []; + return tokens.filter((t) => t !== ABSENT_FILTER_TOKEN); +} + +/** + * The entries relevant to the user's current filter. + * + * With neither axis filtered this returns every entry, which is what makes the + * blank-search view behave exactly as it did on v1 data. Multiple selections on + * an axis are treated as "contains any of them": the ANY/ALL facet mode the user + * picked governs *value* matching, not which scope to read from, so a + * sector=[Power, Steel] selection makes entries for either sector relevant. + */ +export function entriesInScope( + entries: unknown, + query: ScopeQuery, + pathway: PathwayMetadataType, +): ScopedEntry[] { + const all = asEntries(entries); + // The "None" bucket is a predicate about the *pathway* ("has no sectors" / + // "has no geography"), not a scope to read values from, so it must not + // constrain either axis. Dropping it here rather than in each axis helper keeps + // the two consistent: previously geographyScopeOverlaps ignored the token but + // sectorScopeContains compared it as if it were a sector name, so selecting + // Sector=None alongside a keyFeature facet filtered out every entry and made + // the pathway look like it held no values at all. + const sectors = dropAbsent(query.sectors); + const geographies = dropAbsent(query.geographies); + if (sectors.length === 0 && geographies.length === 0) return [...all]; + + const declared = (pathway.sectors ?? []).map((s) => s.name); + + return all.filter((entry) => { + const sectorOk = + sectors.length === 0 || + sectors.some((s) => sectorScopeContains(entry.sector, s, declared)); + if (!sectorOk) return false; + const geoOk = + geographies.length === 0 || + geographies.some((g) => + geographyScopeOverlaps(entry.geography, g, pathway), + ); + return geoOk; + }); +} + +/** Values of the entries relevant to the query — what a facet matches against. */ +export function valuesInScope( + entries: unknown, + query: ScopeQuery, + pathway: PathwayMetadataType, +): string[] { + return entryValues(entriesInScope(entries, query, pathway)); +} + +/** + * How broad a scope axis is, lower being broader. Derived from the token alone so + * this works without pathway context, which is what {@link widestValue}'s callers + * (the rendering components) have available. + */ +function sectorBreadth(sector: string): number { + return sector === CROSS_SECTOR ? 0 : 1; +} + +function geographyBreadth(geography: string): number { + if (geography === GLOBAL_SCOPE) return 0; + if (geography === CROSS_REGION) return 1; + // A two-letter token is a country code; anything longer is a region label. + return /^[A-Za-z]{2}$/.test(geography) ? 3 : 2; +} + +/** + * The value at the broadest scope a field declares. + * + * **Provisional.** This is a placeholder for #869's resolver, which will pick the + * value for the scope the *user* is looking at and report how far it had to + * broaden so #859 can badge it. Until then the components show the widest value, + * which is what v1 effectively showed — every codemod-migrated pathway has exactly + * one entry, at its widest scope — so rendering is unchanged for current data. + * + * Returns `undefined` when nothing is authored at any scope, which the callers + * already render as "No information". + */ +export function widestValue(entries: unknown): string | string[] | undefined { + const all = asEntries(entries); + if (all.length === 0) return undefined; + const ranked = [...all].sort( + (a, b) => + sectorBreadth(a.sector) - sectorBreadth(b.sector) || + geographyBreadth(a.geography) - geographyBreadth(b.geography), + ); + return ranked[0].value; +} diff --git a/src/utils/loadData.test.tsx b/src/utils/loadData.test.tsx index b70ebfe5..bc6e0ab4 100644 --- a/src/utils/loadData.test.tsx +++ b/src/utils/loadData.test.tsx @@ -36,6 +36,35 @@ afterEach(() => { /* ---------------------------- * decideIncludeInvalid unit tests * ---------------------------- */ +describe("isViteDev", () => { + // This gates developer-facing logging, so the case that matters is the one + // where it must stay quiet: a production build. Vite statically replaces + // `import.meta.env.DEV` with false there, which vi.stubEnv can simulate — + // note the globalThis shim used below for decideIncludeInvalid cannot, because + // vitest supplies a real import.meta.env and readViteEnv prefers it. + it("is true when import.meta.env.DEV is set, as on the dev server", async () => { + vi.stubEnv("DEV", true); + const { isViteDev } = await importModule(); + expect(isViteDev()).toBe(true); + }); + + it("is false in a production build", async () => { + vi.stubEnv("DEV", false); + const { isViteDev } = await importModule(); + expect(isViteDev()).toBe(false); + }); + + it("ignores the flags decideIncludeInvalid reads", async () => { + // The two answer different questions; VITE_INCLUDE_INVALID must not make + // a production build report itself as dev. + vi.stubEnv("DEV", false); + vi.stubEnv("VITE_INCLUDE_INVALID", "true"); + vi.stubEnv("NODE_ENV", "development"); + const { isViteDev } = await importModule(); + expect(isViteDev()).toBe(false); + }); +}); + describe("decideIncludeInvalid", () => { it("returns false in production regardless of env flags", async () => { vi.stubEnv("NODE_ENV", "production"); diff --git a/src/utils/loadData.ts b/src/utils/loadData.ts index b57077d0..17e4ea1e 100644 --- a/src/utils/loadData.ts +++ b/src/utils/loadData.ts @@ -28,12 +28,13 @@ type ViteEnv = } | undefined; -// Decider reads both Vite and Node envs so it works in browser & Node contexts. -export function decideIncludeInvalid(): boolean { - // Try to read a Vite-like env if present (works in browser/preview and in tests with a shim) - // Access to import.meta must not use `typeof import` (keyword) — esbuild will error. +/** + * Read a Vite-like env if one is present — the browser/preview build, or a test + * shim. Access to import.meta must not use `typeof import` (keyword), which + * esbuild rejects, hence the try/catch. + */ +function readViteEnv(): ViteEnv { let viteEnv: ViteEnv = undefined; - try { // @ts-expect-error: import.meta is not defined in Node tests viteEnv = import.meta?.env; @@ -45,6 +46,29 @@ export function decideIncludeInvalid(): boolean { viteEnv = (globalThis as typeof globalThis & { import?: ImportMetaEnvShim }) .import?.meta?.env; } + return viteEnv; +} + +/** + * True only when we are confidently running the Vite dev server. + * + * Deliberately conservative: Vite replaces `import.meta.env.DEV` with `false` in + * production builds, and in Node/vitest there is no `import.meta.env` at all, so + * both of those read as "not dev". That is the behaviour wanted for gating + * developer-facing diagnostics — silent in production, and silent under test + * (where src/test/failOnReactWarnings.ts spies on console.warn). + * + * Note this cannot be derived from `decideIncludeInvalid()`: that answers a + * different question (whether to keep schema-invalid pathways) and returns false + * in dev unless VITE_INCLUDE_INVALID is set. + */ +export function isViteDev(): boolean { + return !!readViteEnv()?.DEV; +} + +// Decider reads both Vite and Node envs so it works in browser & Node contexts. +export function decideIncludeInvalid(): boolean { + const viteEnv = readViteEnv(); const viteDev = !!viteEnv?.DEV; const viteFlag = diff --git a/src/utils/searchUtils.scopedFacets.test.ts b/src/utils/searchUtils.scopedFacets.test.ts new file mode 100644 index 00000000..927cc3e2 --- /dev/null +++ b/src/utils/searchUtils.scopedFacets.test.ts @@ -0,0 +1,387 @@ +import { describe, it, expect } from "vitest"; +import { filterPathways, getGlobalFacetOptions } from "./searchUtils"; +import type { FiltersWithArrays } from "./searchUtils"; +import { ABSENT_FILTER_TOKEN } from "./absent"; +import type { PathwayMetadataType } from "../types"; + +/** + * Coverage for the two keyFeature-backed search facets over v2 scoped entries + * (#858): `emissionsTrajectory` and `policyAmbition`. + * + * These arms had no test coverage at all before v2 — no `filterPathways` test + * passed either filter — which is why the v1->v2 shape change broke them + * silently: `concrete.includes(v)` against an array is simply always false, so + * selecting either facet quietly returned zero pathways. + * + * A pathway matches on the value at the scope the user is looking at; holding it + * only at some *other* scope is not a match. The sector axis uses containment; the + * geography axis uses a non-empty ISO intersection, because the query vocabulary + * and each publication's region membership are different lists by design (#783). + * Fallback ranking is #869's job, not this layer's. + */ + +type Entry = { sector: string; geography: string; value: string }; + +function pathway( + id: string, + opts: { + sectors?: string[]; + emissionsTrajectory?: Entry[]; + policyAmbition?: Entry[]; + }, +): PathwayMetadataType { + return { + id, + name: { full: id }, + sectors: (opts.sectors ?? ["Power", "Steel"]).map((name) => ({ + name, + technologies: [], + })), + geography: { + regions: { "South East Asia": ["ID", "TH", "VN"] }, + country: ["US"], + }, + metric: [], + keyFeatures: { + emissionsTrajectory: opts.emissionsTrajectory ?? [], + policyAmbition: opts.policyAmbition ?? [], + }, + } as unknown as PathwayMetadataType; +} + +const e = (sector: string, geography: string, value: string): Entry => ({ + sector, + geography, + value, +}); + +const ids = (list: PathwayMetadataType[]) => list.map((p) => p.id).sort(); + +describe("emissionsTrajectory facet over scoped entries", () => { + const wide = pathway("wide", { + emissionsTrajectory: [e("cross-sector", "Global", "Significant decrease")], + }); + const perSector = pathway("perSector", { + emissionsTrajectory: [ + e("Power", "Global", "Significant decrease"), + e("Steel", "Global", "Minor decrease"), + ], + }); + const regional = pathway("regional", { + emissionsTrajectory: [ + e("cross-sector", "South East Asia", "Significant decrease"), + ], + }); + const empty = pathway("empty", { emissionsTrajectory: [] }); + const all = [wide, perSector, regional, empty]; + + it("matches a value held at the widest scope when nothing is narrowed", () => { + const filters: FiltersWithArrays = { + emissionsTrajectory: ["Significant decrease"], + }; + expect(ids(filterPathways(all, filters))).toEqual([ + "perSector", + "regional", + "wide", + ]); + }); + + it("respects the user's sector: excludes a pathway whose value at that sector differs", () => { + // perSector holds "Significant decrease" for Power but "Minor decrease" for + // Steel. Filtering to Steel must not match it, even though the value exists + // elsewhere in the pathway. This is the whole point of the scope check. + const filters: FiltersWithArrays = { + sector: ["Steel"], + emissionsTrajectory: ["Significant decrease"], + }; + const result = ids(filterPathways(all, filters)); + expect(result).not.toContain("perSector"); + expect(result).toEqual(["regional", "wide"]); + }); + + it("matches that same pathway when the user asks about the sector it holds", () => { + const filters: FiltersWithArrays = { + sector: ["Power"], + emissionsTrajectory: ["Significant decrease"], + }; + expect(ids(filterPathways(all, filters))).toContain("perSector"); + }); + + it("matches a regional entry from a country inside that region", () => { + const filters: FiltersWithArrays = { + geography: ["TH"], + emissionsTrajectory: ["Significant decrease"], + }; + expect(ids(filterPathways(all, filters))).toContain("regional"); + }); + + it("does not match a regional entry from a country outside that region", () => { + // "regional" only has South East Asia data, which does not include the US. + // (The geography facet would also exclude it, but this pins the scope check + // independently — the pathway does declare US coverage.) + const filters: FiltersWithArrays = { + geography: ["US"], + emissionsTrajectory: ["Significant decrease"], + }; + expect(ids(filterPathways(all, filters))).not.toContain("regional"); + }); + + it("treats an empty entry list as the absent bucket", () => { + expect( + ids(filterPathways(all, { emissionsTrajectory: [ABSENT_FILTER_TOKEN] })), + ).toEqual(["empty"]); + }); + + it("does not match the absent bucket when a value is present", () => { + const filters: FiltersWithArrays = { + emissionsTrajectory: [ABSENT_FILTER_TOKEN], + }; + expect(ids(filterPathways(all, filters))).not.toContain("wide"); + }); + + it("passes every pathway through when the facet is not selected", () => { + expect(ids(filterPathways(all, {}))).toEqual([ + "empty", + "perSector", + "regional", + "wide", + ]); + }); + + it("ANY mode matches a pathway holding either selected value", () => { + const filters: FiltersWithArrays = { + emissionsTrajectory: ["Minor decrease", "Moderate decrease"], + modes: { emissionsTrajectory: "ANY" }, + }; + expect(ids(filterPathways(all, filters))).toEqual(["perSector"]); + }); + + it("ALL mode requires every selected value, which multi-scope data can satisfy", () => { + // Newly meaningful in v2: one pathway can genuinely hold two values at once. + const filters: FiltersWithArrays = { + emissionsTrajectory: ["Significant decrease", "Minor decrease"], + modes: { emissionsTrajectory: "ALL" }, + }; + expect(ids(filterPathways(all, filters))).toEqual(["perSector"]); + }); + + it("ALL mode excludes a pathway holding only one of the selected values", () => { + const filters: FiltersWithArrays = { + emissionsTrajectory: ["Significant decrease", "Minor decrease"], + modes: { emissionsTrajectory: "ALL" }, + }; + expect(ids(filterPathways(all, filters))).not.toContain("wide"); + }); + + it("cross-sector does not answer a sector the pathway never declares", () => { + const powerOnly = pathway("powerOnly", { + sectors: ["Power"], + emissionsTrajectory: [ + e("cross-sector", "Global", "Significant decrease"), + ], + }); + const filters: FiltersWithArrays = { + sector: ["Steel"], + emissionsTrajectory: ["Significant decrease"], + }; + // The sector facet excludes it too; asserted here so the scope rule is + // pinned independently of that. + expect(ids(filterPathways([powerOnly], filters))).toEqual([]); + }); +}); + +describe("policyAmbition facet over scoped entries", () => { + const p = pathway("p", { + policyAmbition: [ + e("Power", "Global", "High ambition policies"), + e("Steel", "Global", "Current/legislated policies"), + ], + }); + + it("respects the user's sector, same as emissionsTrajectory", () => { + expect( + ids( + filterPathways([p], { + sector: ["Steel"], + policyAmbition: ["High ambition policies"], + }), + ), + ).toEqual([]); + expect( + ids( + filterPathways([p], { + sector: ["Power"], + policyAmbition: ["High ambition policies"], + }), + ), + ).toEqual(["p"]); + }); + + it("applies independently of the emissionsTrajectory facet", () => { + const filters: FiltersWithArrays = { + policyAmbition: ["Current/legislated policies"], + emissionsTrajectory: ["Significant decrease"], + }; + // emissionsTrajectory is empty on this pathway, so the conjunction fails. + expect(ids(filterPathways([p], filters))).toEqual([]); + }); +}); + +describe("region filters vs publication region membership", () => { + // Regression for the containment bug. The fixture pathway names its region + // "South East Asia" with members [ID, TH, VN]; the filter vocabulary's token is + // "Southeast Asia" with 11 codes. The lists differ in both spelling and + // membership, which is normal (#783) — and under strict containment it meant a + // region filter plus a keyFeature facet returned nothing, even though the + // geography facet alone (which has always used overlap) kept the pathway. + const regional = pathway("regional", { + emissionsTrajectory: [ + e("cross-sector", "South East Asia", "Significant decrease"), + ], + }); + + it("matches when the query region overlaps the entry's region", () => { + expect( + ids( + filterPathways([regional], { + geography: ["Southeast Asia"], + emissionsTrajectory: ["Significant decrease"], + }), + ), + ).toEqual(["regional"]); + }); + + it("agrees with the geography facet applied on its own", () => { + // The two must not disagree about the same pathway — that divergence was the + // bug, and it only showed up once a second filter was added. + const geoOnly = ids( + filterPathways([regional], { geography: ["Southeast Asia"] }), + ); + const combined = ids( + filterPathways([regional], { + geography: ["Southeast Asia"], + emissionsTrajectory: ["Significant decrease"], + }), + ); + expect(geoOnly).toEqual(["regional"]); + expect(combined).toEqual(geoOnly); + }); + + it("still excludes a region that shares no countries with the entry", () => { + expect( + ids( + filterPathways([regional], { + geography: ["North America"], + emissionsTrajectory: ["Significant decrease"], + }), + ), + ).toEqual([]); + }); +}); + +describe("Sector=None combined with a scoped facet", () => { + // Regression for the case Copilot flagged: selecting the "None" sector bucket + // alongside a keyFeature facet used to return nothing, because the ABSENT token + // was compared as though it were a sector name and filtered out every entry. + const noSectors = { + id: "no-sectors", + name: { full: "No Sectors" }, + sectors: [], + geography: { global: true }, + metric: [], + keyFeatures: { + emissionsTrajectory: [ + e("cross-sector", "Global", "Significant decrease"), + ], + policyAmbition: [], + }, + } as unknown as PathwayMetadataType; + + it("matches on the sector bucket alone", () => { + expect( + ids(filterPathways([noSectors], { sector: [ABSENT_FILTER_TOKEN] })), + ).toEqual(["no-sectors"]); + }); + + it("still matches when combined with the keyFeature facet", () => { + expect( + ids( + filterPathways([noSectors], { + sector: [ABSENT_FILTER_TOKEN], + emissionsTrajectory: ["Significant decrease"], + }), + ), + ).toEqual(["no-sectors"]); + }); + + it("does not match a value the pathway does not hold", () => { + expect( + ids( + filterPathways([noSectors], { + sector: [ABSENT_FILTER_TOKEN], + emissionsTrajectory: ["Minor decrease"], + }), + ), + ).toEqual([]); + }); + + it("behaves the same for the geography None bucket", () => { + expect( + ids( + filterPathways([noSectors], { + geography: [ABSENT_FILTER_TOKEN], + emissionsTrajectory: ["Significant decrease"], + }), + ), + ).toEqual([]); // geography facet itself excludes it: this pathway IS global + }); +}); + +describe("getGlobalFacetOptions over scoped entries", () => { + const all = [ + pathway("a", { + emissionsTrajectory: [ + e("cross-sector", "Global", "Significant decrease"), + ], + policyAmbition: [e("cross-sector", "Global", "High ambition policies")], + }), + pathway("b", { + emissionsTrajectory: [ + e("Power", "Global", "Minor decrease"), + e("Steel", "Global", "Significant decrease"), + ], + }), + ]; + + it("lists the union of values across every scope, deduplicated", () => { + const { emissionsTrajectoryOptions } = getGlobalFacetOptions(all); + const values = emissionsTrajectoryOptions + .map((o) => o.value) + .filter((v) => v !== ABSENT_FILTER_TOKEN); + expect([...values].sort()).toEqual([ + "Minor decrease", + "Significant decrease", + ]); + }); + + it("never emits an option built from an entry object", () => { + // The v1->v2 break turned these options into "[object Object]" because the + // entries were passed through verbatim instead of their values. + const { emissionsTrajectoryOptions, policyAmbitionOptions } = + getGlobalFacetOptions(all); + for (const opt of [ + ...emissionsTrajectoryOptions, + ...policyAmbitionOptions, + ]) { + expect(String(opt.label)).not.toContain("object"); + expect(String(opt.value)).not.toContain("object"); + } + }); + + it("offers the absent bucket when a pathway has no entries for the field", () => { + const { policyAmbitionOptions } = getGlobalFacetOptions(all); + expect(policyAmbitionOptions.map((o) => o.value)).toContain( + ABSENT_FILTER_TOKEN, + ); + }); +}); diff --git a/src/utils/searchUtils.ts b/src/utils/searchUtils.ts index 1011ecc2..4c7ef059 100644 --- a/src/utils/searchUtils.ts +++ b/src/utils/searchUtils.ts @@ -14,6 +14,7 @@ import { selectedGeographyToISO, } from "./filterRegions"; import { matchesOptionalFacetAny, matchesOptionalFacetAll } from "./facets"; +import { entryValues, valuesInScope } from "./keyFeatureScope"; import { ABSENT_FILTER_TOKEN } from "./absent"; import { buildOptionsFromValues, hasAbsent, withAbsentOption } from "./facets"; import { sortPathwayType } from "./sortUtils"; @@ -81,15 +82,28 @@ export function getGlobalFacetOptions(pathways: PathwayMetadataType[]) { pathways.map((d) => d.metric).flat(), ); - // emissionsTrajectory - const emissionsTrajectoryOptions = buildOptionsFromValues( - pathways.map((d) => d.keyFeatures.emissionsTrajectory).flat(), - ); + // emissionsTrajectory / policyAmbition (#858: scoped entries, not scalars). + // Options are the union of the values across every scope a pathway declares — + // deliberately unfiltered, so the dropdown lists everything selectable rather + // than shifting as the user narrows sector/geography. + // + // The ABSENT bucket has to be added explicitly, as it is for sectors below. In + // v1 a pathway missing the field contributed `undefined`, which + // buildOptionsFromValues read as absent; in v2 an empty entries array + // contributes *no* elements, so nothing would signal absence and the "None" + // option would vanish even though the filter still honours the token. + const scopedFacetOptions = ( + field: "emissionsTrajectory" | "policyAmbition", + ) => { + const values = pathways.map((d) => entryValues(d.keyFeatures?.[field])); + return withAbsentOption( + buildOptionsFromValues(values.flat()), + values.some((v) => v.length === 0), + ); + }; - // Policy ambition - const policyAmbitionOptions = buildOptionsFromValues( - pathways.map((d) => d.keyFeatures.policyAmbition).flat(), - ); + const emissionsTrajectoryOptions = scopedFacetOptions("emissionsTrajectory"); + const policyAmbitionOptions = scopedFacetOptions("policyAmbition"); const dataAvailabilityOptions = buildOptionsFromValues( pathways.map((d) => availabilityFor(d)).flat(), @@ -435,68 +449,45 @@ export const filterPathways = ( if (!ok) return false; } - // emissionsTrajectory filter + // emissionsTrajectory / policyAmbition filters (#858). + // + // These two are keyFeature fields *and* search facets. In v1 each held one + // scalar; in v2 each holds scoped {sector, geography, value} entries, so a + // pathway can carry several values at once and the question becomes "which + // value, at which scope?". + // + // Answer, per the product decision: the value at the scope the user is + // looking at. `valuesInScope` narrows the entries to those whose scope + // contains the active sector/geography selection, and matching then works + // exactly like the sector and metric facets above — an ANY/ALL match over a + // list of values, with an empty list treated as the ABSENT bucket. A pathway + // holding the selected value only at some *other* scope is not a match. + // + // Containment only: no fallback ranking, no "how far did we broaden" cost. + // That is #869, and it is what will turn a non-match at the queried scope + // into a ranked broader-scope match rather than an exclusion. { - const selected = toArray(filters.emissionsTrajectory); - if (selected.length) { - const hasAbsent = selected.includes(ABSENT_FILTER_TOKEN); - const concrete = selected.filter((t) => t !== ABSENT_FILTER_TOKEN); - const v = pathway.keyFeatures?.emissionsTrajectory ?? null; - const mode = pickMode("emissionsTrajectory", filters.modes); - let ok = true; - - if (mode === "ANY") { - ok = - (v == null && hasAbsent) || - (v != null && (concrete.length ? concrete.includes(v) : false)); - } else { - // ALL: for single-valued fields, all selected tokens must hold. - // That is only possible when exactly one token is selected: - // - [ABSENT] -> v == null - // - [X] -> v == X - // Any combination (ABSENT + X, or X + Y) cannot be satisfied. - if (hasAbsent && concrete.length === 0) { - ok = v == null; - } else if (!hasAbsent && concrete.length === 1) { - ok = v != null && v === concrete[0]; - } else { - ok = false; - } - } - if (!ok) return false; - } - } - - // policyAmbition filter - { - const selected = toArray(filters.policyAmbition); - if (selected.length) { - const hasAbsent = selected.includes(ABSENT_FILTER_TOKEN); - const concrete = selected.filter((t) => t !== ABSENT_FILTER_TOKEN); - const v = pathway.keyFeatures?.policyAmbition ?? null; - const mode = pickMode("policyAmbition", filters.modes); - let ok = true; + const scopeQuery = { + sectors: toArray(filters.sector), + geographies: toArray(filters.geography), + }; + const scopedFacetOk = ( + facet: "emissionsTrajectory" | "policyAmbition", + ): boolean => { + const selected = toArray(filters[facet]); + if (selected.length === 0) return true; + const values = valuesInScope( + pathway.keyFeatures?.[facet], + scopeQuery, + pathway, + ); + return pickMode(facet, filters.modes) === "ALL" + ? matchesOptionalFacetAll(selected, values, (s) => s) + : matchesOptionalFacetAny(selected, values, (s) => s); + }; - if (mode === "ANY") { - ok = - (v == null && hasAbsent) || - (v != null && (concrete.length ? concrete.includes(v) : false)); - } else { - // ALL: for single-valued fields, all selected tokens must hold. - // That is only possible when exactly one token is selected: - // - [ABSENT] -> v == null - // - [X] -> v == X - // Any combination (ABSENT + X, or X + Y) cannot be satisfied. - if (hasAbsent && concrete.length === 0) { - ok = v == null; - } else if (!hasAbsent && concrete.length === 1) { - ok = v != null && v === concrete[0]; - } else { - ok = false; - } - } - if (!ok) return false; - } + if (!scopedFacetOk("emissionsTrajectory")) return false; + if (!scopedFacetOk("policyAmbition")) return false; } // dataAvailability filter diff --git a/src/utils/timeseriesTaxonomy.test.ts b/src/utils/timeseriesTaxonomy.test.ts new file mode 100644 index 00000000..d37ba1b1 --- /dev/null +++ b/src/utils/timeseriesTaxonomy.test.ts @@ -0,0 +1,220 @@ +import { describe, it, expect } from "vitest"; +import { + metricBelongsToSector, + metricsForSector, + POWER_SECTOR_DEFINITION, + SECTORS_BY_KEY, + segmentBelongsToSector, + segmentsForSector, + technologiesForSector, + technologyBelongsToSector, + UNSEGMENTED, +} from "./timeseriesTaxonomy"; +import sectorSchema from "../schema/common/sector.v1.json" with { type: "json" }; +import technologySchema from "../schema/common/technology.v1.json" with { type: "json" }; +import metricSchema from "../schema/common/metric.v1.json" with { type: "json" }; +import segmentSchema from "../schema/common/sectorSegment.v1.json" with { type: "json" }; + +const SECTOR_NAMES: string[] = sectorSchema.$defs.displayName.enum; +const TECHNOLOGY_NAMES: string[] = technologySchema.$defs.displayName.enum; +const METRIC_NAMES: string[] = metricSchema.$defs.displayName.enum; +const SEGMENT_NAMES: string[] = segmentSchema.$defs.displayName.enum; + +describe("technologiesForSector", () => { + it("resolves Power to its ten technologies", () => { + expect(technologiesForSector("Power")).toEqual([ + "Biomass", + "Coal", + "Gas", + "Hydro", + "Nuclear", + "Oil", + "Other", + "Renewables", + "Solar", + "Wind", + ]); + }); + + it("derives the list from POWER_SECTOR_DEFINITION rather than duplicating it", () => { + // Drift guard: the metadata-side allowlist and the chart-side taxonomy are + // the same data, so adding a technology to the definition must be the only + // edit needed. Same intent as the wrapper-consistency tests in + // pathwayMetadata.v2.test.ts. + expect(technologiesForSector("Power")).toEqual( + Object.values(POWER_SECTOR_DEFINITION.technologies).map( + (t) => t.displayName, + ), + ); + }); + + it("returns undefined -- not [] -- for a sector with no definition", () => { + // The distinction is load-bearing: validateScopedEntries rejects a non-empty + // list here, and could not tell "defined and empty" from "undefined" if this + // collapsed to []. + expect(technologiesForSector("Steel")).toBeUndefined(); + }); + + it("returns undefined for a string that is not a sector at all", () => { + expect(technologiesForSector("Narnia")).toBeUndefined(); + }); + + it("keys on display name, not on the camelCase taxonomy key", () => { + // SECTORS_BY_KEY is keyed "power"; metadata says "Power". Passing the key + // must not accidentally work, or data could name sectors either way. + expect(technologiesForSector("power")).toBeUndefined(); + }); + + it("only defines technologies for sectors the metadata schema knows", () => { + for (const sector of Object.values(SECTORS_BY_KEY)) { + expect(SECTOR_NAMES).toContain(sector.displayName); + } + }); + + it("only names technologies the metadata schema allows", () => { + // A displayName here that the enum lacks would be permanently unusable: AJV + // would reject the data before this check ever saw it. + for (const sector of Object.values(SECTORS_BY_KEY)) { + for (const tech of Object.values(sector.technologies)) { + expect(TECHNOLOGY_NAMES).toContain(tech.displayName); + } + } + }); +}); + +describe("technologyBelongsToSector", () => { + it("says yes for a technology of a defined sector", () => { + expect(technologyBelongsToSector("Solar", "Power")).toBe("yes"); + }); + + it("says no for a technology outside a defined sector's list", () => { + expect(technologyBelongsToSector("Hydrogen Use", "Power")).toBe("no"); + }); + + it("says unknown -- not no -- for a sector with no definition", () => { + // The difference matters to non-validation callers: #869's technology axis + // would silently drop every Steel pathway if this answered "no". + expect(technologyBelongsToSector("Hydrogen Use", "Steel")).toBe("unknown"); + expect(technologyBelongsToSector("Solar", "Steel")).toBe("unknown"); + }); + + it("says unknown for an unrecognised sector", () => { + expect(technologyBelongsToSector("Solar", "Narnia")).toBe("unknown"); + }); + + it("is exact about names, not fuzzy", () => { + expect(technologyBelongsToSector("solar", "Power")).toBe("no"); + expect(technologyBelongsToSector("Solar Power", "Power")).toBe("no"); + }); + + it("classifies every enum technology under Power as yes or no, never unknown", () => { + for (const tech of TECHNOLOGY_NAMES) { + expect(technologyBelongsToSector(tech, "Power")).not.toBe("unknown"); + } + }); +}); + +describe("metricsForSector / metricBelongsToSector (#870)", () => { + it("resolves Power to its five metrics", () => { + expect(metricsForSector("Power")).toEqual([ + "Absolute Emissions", + "Capacity", + "Emissions Intensity", + "Generation", + "Technology Mix", + ]); + }); + + it("derives the list from POWER_SECTOR_DEFINITION rather than duplicating it", () => { + expect(metricsForSector("Power")).toEqual( + Object.values(POWER_SECTOR_DEFINITION.metrics).map((m) => m.displayName), + ); + }); + + it("covers the whole metric enum for Power", () => { + // Worth pinning: it is why the sector/metric check cannot currently reject + // a real Power row, and why it only becomes live for production data once a + // second sector defines its metrics. + expect([...(metricsForSector("Power") ?? [])].sort()).toEqual( + [...METRIC_NAMES].sort(), + ); + }); + + it("returns undefined for a sector with no metrics defined", () => { + expect(metricsForSector("Steel")).toBeUndefined(); + }); + + it("says yes for a metric of a defined sector", () => { + expect(metricBelongsToSector("Capacity", "Power")).toBe("yes"); + }); + + it("says no for a metric outside a defined sector's list", () => { + expect(metricBelongsToSector("Water Use", "Power")).toBe("no"); + }); + + it("says unknown for a sector with no metrics defined", () => { + // validateScopedEntries lets "unknown" pass for this axis, unlike the other + // two — see the comment on that check. + expect(metricBelongsToSector("Capacity", "Steel")).toBe("unknown"); + }); + + it("only names metrics the schema allows", () => { + for (const sector of Object.values(SECTORS_BY_KEY)) { + for (const metric of Object.values(sector.metrics)) { + expect(METRIC_NAMES).toContain(metric.displayName); + } + } + }); +}); + +describe("segmentsForSector / segmentBelongsToSector (#870)", () => { + it("resolves Power to its three segments", () => { + expect(segmentsForSector("Power")).toEqual([ + "Power generation", + "Storage", + "Transmission & Distribution", + ]); + }); + + it("returns undefined for a sector with no segments defined", () => { + expect(segmentsForSector("Steel")).toBeUndefined(); + }); + + it("excludes the universal sentinel from the defined list", () => { + // UNSEGMENTED is legal everywhere, so listing it under Power would imply it + // is Power's in particular. + expect(segmentsForSector("Power")).not.toContain(UNSEGMENTED); + }); + + it("says yes for a segment of a defined sector", () => { + expect(segmentBelongsToSector("Storage", "Power")).toBe("yes"); + }); + + it("says no for a segment outside a defined sector's list", () => { + expect(segmentBelongsToSector("Refining", "Power")).toBe("no"); + }); + + it("says unknown for a named segment under an undefined sector", () => { + expect(segmentBelongsToSector("Storage", "Steel")).toBe("unknown"); + }); + + it("says yes to the sentinel under every sector, defined or not", () => { + // This is what keeps dataAvailability authorable before a sector's segments + // are written down. + for (const name of [...SECTOR_NAMES, "Narnia"]) { + expect(segmentBelongsToSector(UNSEGMENTED, name)).toBe("yes"); + } + }); + + it("only names segments the schema allows", () => { + for (const sector of Object.values(SECTORS_BY_KEY)) { + for (const segment of Object.values(sector.segments ?? {})) { + expect(SEGMENT_NAMES).toContain(segment.displayName); + } + } + }); + + it("keeps the sentinel in the schema enum", () => { + expect(SEGMENT_NAMES).toContain(UNSEGMENTED); + }); +}); diff --git a/src/utils/timeseriesTaxonomy.ts b/src/utils/timeseriesTaxonomy.ts index a3adcd38..13200578 100644 --- a/src/utils/timeseriesTaxonomy.ts +++ b/src/utils/timeseriesTaxonomy.ts @@ -11,11 +11,27 @@ export interface MetricDefinition { sectorScope?: string; } +/** + * A subdivision of a sector (#870): Power splits into generation, storage and + * transmission & distribution, and a metric's data availability can differ + * between them. + */ +export interface SegmentDefinition { + displayName: string; + definition: string; +} + export interface SectorDefinition { key: string; displayName: string; technologies: Record; metrics: Record; + /** + * Optional, unlike the two above: segments arrived with #870 and only Power + * has them. Absent means "nobody has written this sector's segments down", + * which the membership check below treats as closed rather than open. + */ + segments?: Record; } export const POWER_SECTOR_DEFINITION: SectorDefinition = { @@ -73,6 +89,23 @@ export const POWER_SECTOR_DEFINITION: SectorDefinition = { "Electricity generation via wind turbines, both onshore and offshore.", }, }, + segments: { + powerGeneration: { + displayName: "Power generation", + definition: + "Generation of electricity at the plant, before it reaches the grid.", + }, + storage: { + displayName: "Storage", + definition: + "Storing electricity for later dispatch (batteries, pumped hydro).", + }, + transmissionAndDistribution: { + displayName: "Transmission & Distribution", + definition: + "Moving electricity from generators to consumers, including grid losses.", + }, + }, metrics: { absoluteEmissions: { displayName: "Absolute Emissions", @@ -156,3 +189,143 @@ export function getTechnologyDefinition( } return tech; } + +/* -------------------------------------------------------------------------- */ +/* Sector-conditional membership (#461 technologies, #870 metrics + segments) */ +/* -------------------------------------------------------------------------- */ + +/** + * Whether some value belongs to a sector. + * + * Tri-state rather than boolean on purpose. Only one of the fifteen sectors in + * `sector.v1.json` has its vocabularies defined here, so a boolean would have to + * answer `false` for the other fourteen — indistinguishable from "definitely not + * this sector's value" and wrong for every caller that is not validation. + * `"unknown"` lets each caller choose: `validateScopedEntries` treats it as a + * failure (see below), while a search filter can treat it as a match rather than + * silently dropping every non-Power pathway. + */ +export type TaxonomyMembership = "yes" | "no" | "unknown"; + +/** @deprecated Use {@link TaxonomyMembership}; kept so #461's callers still compile. */ +export type TechnologyMembership = TaxonomyMembership; + +/** + * Sector definitions indexed by display name. + * + * `SECTORS_BY_KEY` is keyed by the camelCase key the timeseries data uses + * (`power`), while pathway metadata names sectors by their display name + * (`"Power"`, `"Oil (Upstream)"`). `displayName` is the only bridge between the + * two: there is no camelCase↔Title-Case converter in the repo, and there cannot + * be a lossless one — `capitalizeWords` is a one-way chart-label prettifier and + * could never produce `"Oil (Upstream)"` from `oilUpstream`. + */ +const SECTORS_BY_DISPLAY_NAME: ReadonlyMap = new Map( + Object.values(SECTORS_BY_KEY).map((sector) => [sector.displayName, sector]), +); + +/** The `SectorDefinition` axes that carry a sector-conditional vocabulary. */ +type VocabularyAxis = "technologies" | "metrics" | "segments"; + +/** + * The display names a sector defines on one axis, or `undefined` when that + * sector defines nothing on it. + * + * The `undefined` is load-bearing: it separates "this vocabulary is defined and + * happens to be empty" from "nobody has said what this sector's values are". + * Callers must not collapse it to `[]` — that is the difference between a + * checked sector and an unchecked one. + * + * Derived from `SECTORS_BY_KEY` rather than listed separately, so adding a + * sector definition is the single edit that opens it up to metadata as well. + */ +function vocabularyFor( + sectorDisplayName: string, + axis: VocabularyAxis, +): readonly string[] | undefined { + const defined = SECTORS_BY_DISPLAY_NAME.get(sectorDisplayName)?.[axis]; + if (!defined) return undefined; + return Object.values(defined).map((entry) => entry.displayName); +} + +/** + * Closed-by-default membership on one axis. + * + * Note this cannot distinguish "sector with no definition" from "not a sector at + * all" — both are `"unknown"`. That is fine for the callers there are: AJV has + * already checked the sector against `sector.v1.json`'s enum before validation + * reaches here, and a caller that wants to know whether a string is a sector + * should ask the schema, not the taxonomy. + */ +function membership( + value: string, + sectorDisplayName: string, + axis: VocabularyAxis, +): TaxonomyMembership { + const allowed = vocabularyFor(sectorDisplayName, axis); + if (!allowed) return "unknown"; + return allowed.includes(value) ? "yes" : "no"; +} + +/** The technologies defined for a sector (#461). */ +export function technologiesForSector( + sectorDisplayName: string, +): readonly string[] | undefined { + return vocabularyFor(sectorDisplayName, "technologies"); +} + +/** + * #461: scope technologies to their sector. Both arguments are display names, as + * they appear in pathway metadata's `sectors[]`. + */ +export function technologyBelongsToSector( + technologyDisplayName: string, + sectorDisplayName: string, +): TaxonomyMembership { + return membership(technologyDisplayName, sectorDisplayName, "technologies"); +} + +/** The metrics defined for a sector (#870). */ +export function metricsForSector( + sectorDisplayName: string, +): readonly string[] | undefined { + return vocabularyFor(sectorDisplayName, "metrics"); +} + +/** + * #870: a `dataAvailability` row may only describe a metric its sector actually + * has. Power's five metrics are currently the whole of `metric.v1.json`'s enum, + * so this bites on the other fourteen sectors rather than on Power. + */ +export function metricBelongsToSector( + metricDisplayName: string, + sectorDisplayName: string, +): TaxonomyMembership { + return membership(metricDisplayName, sectorDisplayName, "metrics"); +} + +/** + * The segment every sector accepts, whatever its definition says (#870). + * + * Without this, closed-by-default would make `dataAvailability` unauthorable for + * the fourteen sectors whose segments nobody has written down yet — which would + * turn a validation aid into a content blocker. Mirrors the `"No information"` + * sentinel the v2 keyFeature enums already use. + */ +export const UNSEGMENTED = "No information"; + +/** The segments defined for a sector (#870), excluding the universal sentinel. */ +export function segmentsForSector( + sectorDisplayName: string, +): readonly string[] | undefined { + return vocabularyFor(sectorDisplayName, "segments"); +} + +/** #870: scope segments to their sector, with {@link UNSEGMENTED} always legal. */ +export function segmentBelongsToSector( + segmentDisplayName: string, + sectorDisplayName: string, +): TaxonomyMembership { + if (segmentDisplayName === UNSEGMENTED) return "yes"; + return membership(segmentDisplayName, sectorDisplayName, "segments"); +} diff --git a/src/utils/validateData.test.tsx b/src/utils/validateData.test.tsx index 0e53e3df..cad013e5 100644 --- a/src/utils/validateData.test.tsx +++ b/src/utils/validateData.test.tsx @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { validateDataCollect, FileEntry } from "./validateData"; import { PathwayMetadataType } from "../types"; import pathwayMetadata from "../schema/pathwayMetadata.v1.json" with { type: "json" }; +import pathwayMetadataV2 from "../schema/pathwayMetadata.v2.json" with { type: "json" }; import { commonSchemas } from "../schema/common"; function ok(entry: FileEntry | FileEntry[]) { @@ -30,7 +31,24 @@ function fail(entry: FileEntry | FileEntry[], rx?: RegExp | string) { } } +/** Same as {@link fail}, but routes the document against v2. */ +function fail2(entry: FileEntry | FileEntry[], rx?: RegExp | string) { + const arr = Array.isArray(entry) ? entry : [entry]; + const { invalid } = validateDataCollect( + arr, + pathwayMetadataV2, + commonSchemas, + ); + expect(invalid.length).toBeGreaterThan(0); + if (rx) { + const messages = invalid.flatMap((p) => p.errors).join("\n"); + expect(messages).toMatch(rx); + } +} + import basePathway from "../../testdata/valid/pathwayMetadata_standard.json" assert { type: "json" }; +import v2Full from "../../testdata/valid/pathwayMetadata_v2_full.json" assert { type: "json" }; +import v2Minimal from "../../testdata/valid/pathwayMetadata_v2_minimal.json" assert { type: "json" }; describe("pathway schema enforces expected limits", () => { it("accepts a valid object", () => { @@ -287,3 +305,216 @@ describe("pathway schema enforces expected limits", () => { ); }); }); + +describe("v1 and v2 documents coexist (#858)", () => { + const v1Entry: FileEntry = { name: "v1.json", data: basePathway }; + const v2Entry: FileEntry = { name: "v2.json", data: v2Full }; + const v2MinEntry: FileEntry = { name: "v2-min.json", data: v2Minimal }; + + it("validates a v1 document against v1", () => { + const { valid, invalid } = validateDataCollect( + [v1Entry], + pathwayMetadata, + commonSchemas, + ); + expect(invalid).toHaveLength(0); + expect(valid).toHaveLength(1); + }); + + it("validates v2 documents against v2, including all-empty keyFeatures", () => { + const { valid, invalid } = validateDataCollect( + [v2Entry, v2MinEntry], + pathwayMetadataV2, + commonSchemas, + ); + expect(invalid).toHaveLength(0); + expect(valid).toHaveLength(2); + }); + + it("routes by the document's own $schema, so a mixed corpus splits cleanly", () => { + const mixed = [v1Entry, v2Entry]; + expect( + validateDataCollect(mixed, pathwayMetadata, commonSchemas).valid.map( + (r) => r.name, + ), + ).toEqual(["v1.json"]); + expect( + validateDataCollect(mixed, pathwayMetadataV2, commonSchemas).valid.map( + (r) => r.name, + ), + ).toEqual(["v2.json"]); + }); + + it("SILENTLY DROPS documents of the other version — neither valid nor invalid", () => { + // Load-bearing behaviour, not a bug to fix here: validateDataCollect filters + // entries to the one $id it was handed. It is what lets v1 and v2 sit in + // src/data together, and it is also why pointing the loader at v2 makes every + // un-migrated v1 file disappear from the app with no error. Whatever calls + // this has to report the count it dropped, or 49 missing pathways look like a + // data bug. + const { valid, invalid } = validateDataCollect( + [v2Entry], + pathwayMetadata, + commonSchemas, + ); + expect(valid).toHaveLength(0); + expect(invalid).toHaveLength(0); + }); + + it("keeps v2's scoped keyFeatures out of v1 and vice versa", () => { + // A v1-shaped scalar is not a legal v2 value... + fail2( + { + name: "scalar-in-v2.json", + data: { ...v2Full, keyFeatures: basePathway.keyFeatures }, + }, + /keyFeatures/, + ); + // ...and a v2-shaped array is not a legal v1 value. + fail( + { + name: "array-in-v1.json", + data: { ...basePathway, keyFeatures: v2Full.keyFeatures }, + }, + /keyFeatures/, + ); + }); + + it("rejects a v2 document that still carries the removed overview fields", () => { + fail2( + { + name: "expert-overview-in-v2.json", + data: { ...v2Full, expertOverview: "Should not be here." }, + }, + /must NOT have additional properties/, + ); + }); +}); + +describe("v2 enforces its own required fields (#858)", () => { + // The v1 REQ block above deliberately still lists `expertOverview`: those cases + // validate v1 documents against v1, where it remains required. These are the v2 + // counterparts. `coreDrivers` and `dependencies` matter most — the codemod + // scaffolds them to null/[], so they are the fields most likely to be omitted + // by hand-authoring or by a partial migration of the remaining files. + const V2_REQ = [ + "id", + "name", + "description", + "publication", + "pathwayType", + "geography", + "sectors", + "pathwayDescription", + "metric", + "keyFeatures", + "coreDrivers", + "dependencies", + ]; + + for (const key of V2_REQ) { + it(`fails when required property '${key}' is missing`, () => { + const rest: Record = { ...v2Full }; + delete rest[key]; + fail2({ name: "missing.json", data: rest }, new RegExp(key)); + }); + } + + it("accepts a null pathwayDescription, but not a missing one", () => { + // Nullable-but-required: "we have no description" is authored explicitly, + // which is the same distinction #858 draws for coreDrivers and keyFeatures. + const { invalid } = validateDataCollect( + [ + { + name: "null-desc.json", + data: { ...v2Full, pathwayDescription: null }, + }, + ], + pathwayMetadataV2, + commonSchemas, + ); + expect(invalid).toHaveLength(0); + }); + + it("requires every one of the 7 coreDrivers keys, nullable though they are", () => { + const { coreDrivers } = v2Full as unknown as { + coreDrivers: Record; + }; + for (const key of Object.keys(coreDrivers)) { + const partial = { ...coreDrivers }; + delete partial[key]; + fail2( + { + name: `missing-driver-${key}.json`, + data: { ...v2Full, coreDrivers: partial }, + }, + new RegExp(key), + ); + } + }); + + it("rejects an unknown coreDrivers key", () => { + fail2( + { + name: "extra-driver.json", + data: { + ...v2Full, + coreDrivers: { + ...(v2Full as unknown as { coreDrivers: object }).coreDrivers, + madeUpDriver: "Nope.", + }, + }, + }, + /must NOT have additional properties/, + ); + }); + + it("rejects a dependency scoped to an unknown sector", () => { + fail2( + { + name: "bad-dep-sector.json", + data: { + ...v2Full, + dependencies: [ + { + dependency_name: "Technology", + dependency_description: "Needs something.", + sector: "Yak Shaving", + evidence_type: "Qualitative", + }, + ], + }, + }, + /allowed values/, + ); + }); + + it("rejects an unknown dependency_name or evidence_type", () => { + const base = { + dependency_name: "Technology", + dependency_description: "Needs something.", + sector: "Power", + evidence_type: "Qualitative", + }; + fail2( + { + name: "bad-dep-name.json", + data: { + ...v2Full, + dependencies: [{ ...base, dependency_name: "Vibes" }], + }, + }, + /allowed values/, + ); + fail2( + { + name: "bad-evidence.json", + data: { + ...v2Full, + dependencies: [{ ...base, evidence_type: "Hearsay" }], + }, + }, + /allowed values/, + ); + }); +}); diff --git a/src/utils/validateScopes.test.ts b/src/utils/validateScopes.test.ts new file mode 100644 index 00000000..4d3f6714 --- /dev/null +++ b/src/utils/validateScopes.test.ts @@ -0,0 +1,670 @@ +import { describe, it, expect } from "vitest"; +import { validateScopedEntries } from "./validateScopes"; +import type { PathwayMetadataV2 } from "../types"; +import sectorSchema from "../schema/common/sector.v1.json" with { type: "json" }; + +const SECTOR_NAMES: string[] = sectorSchema.$defs.displayName.enum; + +/** + * These cover the cross-field constraint that JSON Schema draft-07 cannot express + * (see validateScopes.ts). AJV already guarantees the shape, so the fixtures here + * only need the fields the check actually reads — hence the casts. + */ +function pathway(over: Partial): PathwayMetadataV2 { + return { + sectors: [ + { name: "Power", technologies: [] }, + { name: "Steel", technologies: [] }, + ], + geography: { + regions: { "South East Asia": ["TH", "VN"] }, + country: ["SG"], + }, + keyFeatures: {}, + dependencies: [], + ...over, + } as unknown as PathwayMetadataV2; +} + +function entry(sector: string, geography: string) { + return { sector, geography, value: "No information" }; +} + +describe("validateScopedEntries — sector axis", () => { + it("accepts a sector the pathway declares", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("Power", "South East Asia")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); + + it("accepts the cross-sector sentinel", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("cross-sector", "South East Asia")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); + + it("rejects a sector the pathway does not declare", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("Cement", "South East Asia")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/keyFeatures/emissionsTrajectory/0/sector"); + expect(errors[0]).toContain('"Cement"'); + }); + + it("accepts cross-sector on a single-sector pathway (documented non-check)", () => { + const errors = validateScopedEntries( + pathway({ + sectors: [{ name: "Power", technologies: [] }], + keyFeatures: { + emissionsTrajectory: [entry("cross-sector", "South East Asia")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); +}); + +describe("validateScopedEntries — geography axis", () => { + it.each(["cross-region", "South East Asia", "SG", "TH"])( + "accepts %s", + (geography) => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("Power", geography)], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }, + ); + + it("accepts Global only when the pathway actually is global", () => { + const globalPathway = pathway({ + geography: { global: true, regions: { "South East Asia": ["TH"] } }, + }); + expect( + validateScopedEntries({ + ...globalPathway, + keyFeatures: { + emissionsTrajectory: [entry("Power", "Global")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ).toEqual([]); + }); + + it("rejects Global on a pathway that does not set geography.global", () => { + // A South-East-Asia-only pathway carrying a global-scoped value would be + // describing coverage it never claims. #858's "or the widest sentinel" + // wording allows it read literally; that defeats the point of the check. + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("Power", "Global")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('"Global"'); + }); + + it("accepts a country reached only through a declared region", () => { + // The pathway declares "South East Asia": ["TH","VN"] but no standalone VN, + // so scoping to VN is *narrower* than the declaration, not outside it. + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("Power", "VN")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); + + it("rejects a mistyped region label", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("Power", "Souteast Asia")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/keyFeatures/emissionsTrajectory/0/geography"); + expect(errors[0]).toContain('"Souteast Asia"'); + }); + + it("rejects a country the pathway does not cover", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("Power", "DE")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('"DE"'); + }); +}); + +describe("validateScopedEntries — reporting", () => { + it("reports both axes of a single bad entry, and indexes each entry", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [ + entry("Power", "South East Asia"), + entry("Cement", "Narnia"), + ], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toHaveLength(2); + expect(errors.every((e) => e.includes("/emissionsTrajectory/1/"))).toBe( + true, + ); + }); + + it("checks every keyFeatures field, not just the first", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [entry("Power", "South East Asia")], + policyAmbition: [entry("Cement", "South East Asia")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/keyFeatures/policyAmbition/0/sector"); + }); + + it("passes an empty entries array — absent at every scope is legal", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); +}); + +describe("validateScopedEntries — one value per scope", () => { + const dup = (geography: string, value: string) => ({ + sector: "Power", + geography, + value, + }); + + it("rejects two entries at the same scope with different values", () => { + // uniqueItems compares whole entries, so these are "unique" to the schema. + // Left unchecked, the resolver displays one while search matches both. + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [ + dup("South East Asia", "Significant decrease"), + dup("South East Asia", "Minor increase"), + ], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/keyFeatures/emissionsTrajectory/1"); + expect(errors[0]).toContain("duplicates the scope of"); + // Names the entry it collides with, so the fix is obvious in a long list. + expect(errors[0]).toContain("/keyFeatures/emissionsTrajectory/0"); + }); + + it("accepts the same value at genuinely different scopes", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [ + dup("South East Asia", "Significant decrease"), + dup("SG", "Significant decrease"), + ], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); + + it("distinguishes scopes that differ only by sector", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [ + { sector: "Power", geography: "SG", value: "Minor increase" }, + { sector: "Steel", geography: "SG", value: "Minor decrease" }, + ], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); + + it("reports every repeat, not just the second", () => { + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [ + dup("SG", "Significant decrease"), + dup("SG", "Minor increase"), + dup("SG", "Low or no change"), + ], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toHaveLength(2); + // Both point back at index 0 rather than chaining 1->2. + expect(errors.every((e) => e.includes("emissionsTrajectory/0"))).toBe(true); + }); + + it("scopes the check per field, not across the whole object", () => { + // The same (sector, geography) in two different fields is normal. + const errors = validateScopedEntries( + pathway({ + keyFeatures: { + emissionsTrajectory: [dup("SG", "Significant decrease")], + policyAmbition: [dup("SG", "High ambition policies")], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); + + it("does not confuse scopes whose parts concatenate alike", () => { + // Guards the composite key: "A" + "BC" must not collide with "AB" + "C". + const errors = validateScopedEntries( + pathway({ + sectors: [{ name: "Power", technologies: [] }], + geography: { regions: { "Power SG": ["SG"] }, country: ["SG"] }, + keyFeatures: { + emissionsTrajectory: [ + { sector: "Power", geography: "SG", value: "Minor increase" }, + { sector: "Power", geography: "Power SG", value: "Minor decrease" }, + ], + } as unknown as PathwayMetadataV2["keyFeatures"], + }), + ); + expect(errors).toEqual([]); + }); +}); + +describe("validateScopedEntries — dependencies", () => { + it("accepts a declared sector", () => { + const errors = validateScopedEntries( + pathway({ + dependencies: [ + { + dependency_name: "Technology", + dependency_description: "Needs grid upgrades.", + sector: "Power", + evidence_type: "Qualitative", + }, + ] as unknown as PathwayMetadataV2["dependencies"], + }), + ); + expect(errors).toEqual([]); + }); + + it("rejects an undeclared sector", () => { + const errors = validateScopedEntries( + pathway({ + dependencies: [ + { + dependency_name: "Technology", + dependency_description: "Needs grid upgrades.", + sector: "Aviation", + evidence_type: "Qualitative", + }, + ] as unknown as PathwayMetadataV2["dependencies"], + }), + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/dependencies/0/sector"); + }); + + it("rejects the cross-sector sentinel, which is not legal here", () => { + const errors = validateScopedEntries( + pathway({ + dependencies: [ + { + dependency_name: "Technology", + dependency_description: "Needs grid upgrades.", + sector: "cross-sector", + evidence_type: "Qualitative", + }, + ] as unknown as PathwayMetadataV2["dependencies"], + }), + ); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('"cross-sector"'); + }); +}); + +describe("validateScopedEntries — technologies belong to their sector (#461)", () => { + const withSectors = (sectors: unknown) => + validateScopedEntries( + pathway({ sectors: sectors as PathwayMetadataV2["sectors"] }), + ); + + it("accepts technologies the sector lists", () => { + expect( + withSectors([{ name: "Power", technologies: ["Solar", "Wind", "Coal"] }]), + ).toEqual([]); + }); + + it("accepts every technology Power defines", () => { + expect( + withSectors([ + { + name: "Power", + technologies: [ + "Biomass", + "Coal", + "Gas", + "Hydro", + "Nuclear", + "Oil", + "Other", + "Renewables", + "Solar", + "Wind", + ], + }, + ]), + ).toEqual([]); + }); + + it("rejects a technology outside its sector's list, naming the value", () => { + const errors = withSectors([ + { name: "Power", technologies: ["Solar", "Hydrogen Use"] }, + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/sectors/0/technologies/1"); + expect(errors[0]).toContain('"Hydrogen Use"'); + expect(errors[0]).toContain('"Power"'); + }); + + it("accepts an empty list for every sector the schema allows", () => { + // The legal state for the 14 sectors whose technologies are not defined yet, + // and what all four TransitionZero pathways carry today. + for (const name of SECTOR_NAMES) { + expect(withSectors([{ name, technologies: [] }])).toEqual([]); + } + }); + + it("rejects a non-empty list on a sector with no definition", () => { + // Closed by default. Passing this through would mean the next data round + // populates a sector's technologies and nothing checks them. + const errors = withSectors([ + { name: "Steel", technologies: ["Hydrogen Use"] }, + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/sectors/0/technologies"); + expect(errors[0]).toContain('"Steel"'); + }); + + it("names the fix, since the offending data may well be correct", () => { + // The likeliest cause is a sector whose taxonomy nobody has written down + // yet, so the message has to say where to write it. + const errors = withSectors([ + { name: "Cement", technologies: ["Carbon Capture and Storage"] }, + ]); + expect(errors[0]).toContain("SECTORS_BY_KEY"); + expect(errors[0]).toContain("timeseriesTaxonomy.ts"); + expect(errors[0]).toContain('"Carbon Capture and Storage"'); + }); + + it("reports one error per undefined sector, not one per technology", () => { + // The fix is a single edit -- define the sector -- so listing its + // technologies individually would be noise. + const errors = withSectors([ + { name: "Steel", technologies: ["Hydrogen Use", "Electrification"] }, + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain('"Electrification"'); + expect(errors[0]).toContain('"Hydrogen Use"'); + }); + + it("indexes each sector, and checks them all", () => { + const errors = withSectors([ + { name: "Power", technologies: ["Solar"] }, + { name: "Steel", technologies: ["Hydrogen Use"] }, + { name: "Cement", technologies: [] }, + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/sectors/1/technologies"); + }); + + it("passes a pathway with no sectors at all", () => { + // AJV does not require `sectors`, so the check must not assume it is there. + expect(withSectors(undefined)).toEqual([]); + }); +}); + +describe("validateScopedEntries — dataAvailability rows (#870)", () => { + /** A row that passes every check, for tests to vary one field of. */ + const ROW = { + metricName: "Capacity", + sector: "Power", + sectorSegment: "Power generation", + geography: "South East Asia", + geographyCoverage: "Regional", + timeResolution: "1-year", + dataFormat: "In tool", + access: null, + granularity: ["Solar"], + scopeLimitations: null, + }; + + const withRows = (rows: unknown[], over: Record = {}) => + validateScopedEntries( + pathway({ + metric: ["Capacity", "Generation"], + dataAvailability: { overall: null, byMetric: rows }, + ...over, + } as unknown as Partial), + ); + + const row = (over: Record = {}) => ({ ...ROW, ...over }); + + it("accepts a row that satisfies every check", () => { + expect(withRows([row()])).toEqual([]); + }); + + it("passes a pathway with no dataAvailability at all", () => { + // Optional field: authoring is incremental, so absence is not a defect. + expect(validateScopedEntries(pathway({}))).toEqual([]); + }); + + it("passes an empty byMetric array", () => { + expect(withRows([])).toEqual([]); + }); + + it("rejects a sector the pathway does not declare", () => { + const errors = withRows([row({ sector: "Cement" })]); + expect( + errors.some((e) => e.includes("/sector") && e.includes('"Cement"')), + ).toBe(true); + }); + + it("rejects a geography the pathway does not cover", () => { + const errors = withRows([row({ geography: "Narnia" })]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/dataAvailability/byMetric/0/geography"); + expect(errors[0]).toContain('"Narnia"'); + }); + + it("rejects a metric the pathway does not report", () => { + // The likeliest typo of all: `metric` and `dataAvailability` are authored + // separately, so they drift. + const errors = withRows([row({ metricName: "Absolute Emissions" })]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/metricName"); + expect(errors[0]).toContain('"Absolute Emissions"'); + expect(errors[0]).toContain("is not a metric this pathway reports"); + }); + + it("rejects a metric that is not the sector's, naming the value", () => { + // Reached directly here: in production AJV rejects a non-enum metricName + // first, and Power currently defines all five members of that enum, so this + // path only becomes live for real data once a second sector defines metrics. + const errors = withRows([row({ metricName: "Water Use" })], { + metric: ["Water Use"], + }); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("is not a metric of sector"); + expect(errors[0]).toContain('"Water Use"'); + }); + + it("accepts a metric under a sector whose metrics are undefined", () => { + // Steel has no metric list. Rejecting here would make dataAvailability + // unauthorable for 14 of 15 sectors while adding no safety, since the + // metric is still checked against the pathway's own `metric` array. + expect( + withRows([ + row({ + sector: "Steel", + sectorSegment: "No information", + granularity: null, + }), + ]), + ).toEqual([]); + }); + + it("rejects a named segment under a sector with no segments defined", () => { + const errors = withRows([ + row({ sector: "Steel", sectorSegment: "Storage", granularity: null }), + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/sectorSegment"); + expect(errors[0]).toContain('"Storage"'); + // Names the one segment that is legal, and how to define the rest. + expect(errors[0]).toContain('"No information"'); + expect(errors[0]).toContain("SECTORS_BY_KEY"); + }); + + it("accepts the No information segment under any sector", () => { + expect(withRows([row({ sectorSegment: "No information" })])).toEqual([]); + }); + + it("rejects granularity that is not a technology of the sector", () => { + const errors = withRows([row({ granularity: ["Solar", "Hydrogen Use"] })]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/granularity/1"); + expect(errors[0]).toContain('"Hydrogen Use"'); + }); + + it("rejects granularity on a sector with no technologies defined", () => { + const errors = withRows([ + row({ + sector: "Steel", + sectorSegment: "No information", + granularity: ["Solar"], + }), + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("no technologies are defined"); + }); + + it("accepts null granularity — a metric need not be broken down", () => { + expect(withRows([row({ granularity: null })])).toEqual([]); + }); + + it("rejects a paywall on data we host ourselves", () => { + const errors = withRows([ + row({ dataFormat: "In tool", access: "Paywalled" }), + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/access"); + expect(errors[0]).toContain("we host this data"); + }); + + it("rejects a null access on data held at the publisher", () => { + const errors = withRows([ + row({ dataFormat: "Text in publication", access: null }), + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/access"); + expect(errors[0]).toContain('"Free" or "Paywalled"'); + }); + + it("accepts both publisher access values", () => { + for (const access of ["Free", "Paywalled"]) { + expect( + withRows([row({ dataFormat: "Tabular in publication", access })]), + ).toEqual([]); + } + }); + + it("rejects two rows at the same scope", () => { + // uniqueItems permits these: they agree on the scope and differ elsewhere, + // so the schema sees two distinct entries. The table has one cell to render + // them in. + const errors = withRows([ + row(), + row({ timeResolution: "5-year", granularity: null }), + ]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/dataAvailability/byMetric/1"); + expect(errors[0]).toContain("duplicates the scope of"); + expect(errors[0]).toContain("/dataAvailability/byMetric/0"); + }); + + it.each([ + ["metric", { metricName: "Generation" }], + ["segment", { sectorSegment: "Storage" }], + ["geography", { geography: "SG" }], + [ + "sector", + { sector: "Steel", sectorSegment: "No information", granularity: null }, + ], + ])("treats rows differing only by %s as distinct scopes", (_axis, over) => { + expect(withRows([row(), row(over)])).toEqual([]); + }); + + it("does not confuse scopes whose parts concatenate alike", () => { + // Guards the NUL-joined key the same way the keyFeatures test does. + expect( + withRows([ + row({ sectorSegment: "Power generation", geography: "SG" }), + row({ sectorSegment: "No information", geography: "SG" }), + ]), + ).toEqual([]); + }); + + it("indexes each row, and checks them all", () => { + const errors = withRows([row(), row({ geography: "Narnia" })]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("/byMetric/1/geography"); + }); + + it("reports every failing check on one row", () => { + const errors = withRows([ + row({ sector: "Cement", geography: "Narnia", access: "Free" }), + ]); + // undeclared sector, unresolvable geography, granularity under an undefined + // sector, and a paywall on hosted data. + expect(errors.length).toBeGreaterThanOrEqual(4); + expect(errors.every((e) => e.includes("/byMetric/0"))).toBe(true); + }); +}); diff --git a/src/utils/validateScopes.ts b/src/utils/validateScopes.ts new file mode 100644 index 00000000..130f0274 --- /dev/null +++ b/src/utils/validateScopes.ts @@ -0,0 +1,364 @@ +/** + * Structural checks for v2 scoped keyFeatures entries (#858). + * + * #858 requires an entry's `sector`/`geography` to be declared in the pathway's + * own `sectors`/`geography`, or be the widest sentinel. That constraint spans + * sibling data with dynamic keys (`geography.regions` is an open object of + * author-defined labels), which JSON Schema draft-07 cannot express: there is no + * way to point an `enum` at another part of the same document, and AJV's `$data` + * can only reference a single value, not compute the union of region labels, + * region members, and country codes that this needs. + * + * So `scopeGeography.v2.json` validates the *shape* — a non-blank, non-3-letter + * string — and this module validates the *reference*. Without it a mistyped + * region label ("Souteast Asia") would validate cleanly and then silently match + * nothing at search time, which is the worst of both worlds. + * + * It also enforces one-value-per-scope, which `uniqueItems` cannot: that keyword + * compares whole entries, so two at the same (sector, geography) with different + * values are "unique" to the schema while being contradictory as data. + * + * It also enforces #461 -- that a technology belongs to the sector it is attached + * to. #461 suggests mirroring the `if`/`then` sector conditional in + * `pathwayTimeseries.v1.json`, but that keyword pair defeats + * `json-schema-to-typescript`: the timeseries `data` items use exactly that shape + * and generate as `{ [k: string]: unknown }[]`. Applying it to `sectors.items` + * would collapse today's `{ name; technologies }` object type and break every + * consumer of `Sector`, so the constraint lives here with its siblings instead. + * + * #870's `dataAvailability` rows are checked here for the same reasons: their + * `metricName`/`sectorSegment`/`granularity` vocabularies are sector-conditional, + * their scope must resolve against the pathway's own coverage, and `access` is + * tied to a sibling property -- none of which draft-07 can express. + * + * Run from `scripts/schema-check-files.ts`, so `npm run schema:check` gates all + * of it. + * + * Errors are formatted like AJV's (` `) so callers can + * merge them into the same `ValidationProblem.errors` list without special-casing. + */ +import type { PathwayMetadataV2 } from "../types/pathwayMetadata.v2"; +import { + metricBelongsToSector, + metricsForSector, + segmentBelongsToSector, + segmentsForSector, + technologiesForSector, + technologyBelongsToSector, + UNSEGMENTED, +} from "./timeseriesTaxonomy.ts"; + +/** `$id` of the schema these checks apply to. */ +export const PATHWAY_METADATA_V2_ID = + "http://pathways.rmi.org/schema/pathwayMetadata.v2.json"; + +/** Sector sentinel meaning "the union of this pathway's own declared sectors". */ +export const CROSS_SECTOR = "cross-sector"; + +/** Geography sentinels: widest possible, and a multi-region non-global aggregate. */ +export const GLOBAL_SCOPE = "Global"; +export const CROSS_REGION = "cross-region"; + +type ScopedEntry = { sector: string; geography: string; value: unknown }; + +/** + * Every geography token an entry on this pathway may legitimately name: each + * declared region label, every country inside those regions, every standalone + * country, and the sentinels where they apply. Region members count because a + * pathway that covers "South East Asia" does cover Thailand — scoping an entry + * to `TH` is more specific than the pathway's own declaration, not outside it. + * + * `Global` is allowed only when the pathway actually sets `geography.global`. + * #858 phrases the rule as "declared by the pathway, or the widest sentinel", + * which read literally would let a South-East-Asia-only pathway carry a + * global-scoped value — describing coverage it never claims, and defeating the + * point of the check. `cross-region` stays unconditional: #858 reserves it for a + * multi-region non-global aggregate without saying when it applies, and no file + * in the corpus uses it yet, so gating it would be inventing a rule. + */ +function allowedGeographies(pathway: PathwayMetadataV2): Set { + const allowed = new Set([CROSS_REGION]); + const geo = pathway.geography; + if (!geo || typeof geo !== "object") return allowed; + if (geo.global === true) allowed.add(GLOBAL_SCOPE); + if (geo.regions) { + for (const [label, members] of Object.entries(geo.regions)) { + allowed.add(label); + if (Array.isArray(members)) members.forEach((m) => allowed.add(m)); + } + } + if (Array.isArray(geo.country)) geo.country.forEach((c) => allowed.add(c)); + return allowed; +} + +/** + * Sectors an entry may name: the pathway's own, plus the `cross-sector` + * sentinel. + * + * Note what is deliberately *not* checked: #858 remarks that `cross-sector` is + * "only meaningful for multi-sector pathways", but a single-sector pathway using + * it is harmless — it resolves to that one sector — so flagging it would be a + * false positive on a legal document rather than a caught mistake. + */ +function declaredSectors(pathway: PathwayMetadataV2): Set { + const declared = new Set(); + for (const s of pathway.sectors ?? []) { + if (s?.name) declared.add(s.name); + } + return declared; +} + +/** + * The trailing clause of a "not a valid X of sector Y" message. + * + * Three cases, because they call for three different actions. Undefined means + * nobody has written that sector's vocabulary down, and the fix is to write it. + * Defined-but-empty means someone has, and the answer is genuinely "none" -- so + * saying "(allowed: )" would read as a bug in the checker rather than an answer. + * Only a non-empty list can usefully be listed. + */ +function allowedClause( + defined: readonly string[] | undefined, + axis: string, +): string { + if (!defined) { + return ( + `-- no ${axis} are defined for that sector. Add them to SECTORS_BY_KEY in` + + ` src/utils/timeseriesTaxonomy.ts.` + ); + } + if (defined.length === 0) { + return `-- that sector defines no ${axis}.`; + } + return `(allowed: ${quote(defined)})`; +} + +function quote(values: Iterable): string { + return [...values] + .sort((a, b) => a.localeCompare(b)) + .map((v) => `"${v}"`) + .join(", "); +} + +/** + * Check one v2 metadata document's scope references. Returns an empty array when + * everything resolves. Assumes the document already passed AJV against + * `pathwayMetadata.v2.json`, so shapes are trusted and only references are tested. + */ +export function validateScopedEntries(pathway: PathwayMetadataV2): string[] { + const errors: string[] = []; + const declared = declaredSectors(pathway); + const entrySectors = new Set([CROSS_SECTOR, ...declared]); + const geographies = allowedGeographies(pathway); + + const keyFeatures = (pathway.keyFeatures ?? {}) as Record< + string, + ScopedEntry[] | undefined + >; + for (const [field, entries] of Object.entries(keyFeatures)) { + if (!Array.isArray(entries)) continue; + // Tracks the first index each (sector, geography) pair was seen at, so a + // repeat can name its twin. See the duplicate check below for why. + const seenScopes = new Map(); + entries.forEach((entry, i) => { + const at = `/keyFeatures/${field}/${i}`; + if (!entrySectors.has(entry.sector)) { + errors.push( + `${at}/sector "${entry.sector}" is not a sector this pathway declares` + + ` (allowed: ${quote(entrySectors)})`, + ); + } + if (!geographies.has(entry.geography)) { + errors.push( + `${at}/geography "${entry.geography}" is not a geography this pathway` + + ` declares (allowed: ${quote(geographies)})`, + ); + } + + // One scope, one value. The schema's `uniqueItems` only rejects entries + // that are identical including their value, so two entries at the same + // (sector, geography) carrying *different* values validate cleanly -- and + // then disagree downstream: the resolver picks one to display while search + // matches the field under both, so a pathway surfaces under a value its + // own detail page does not show. The likely author intent is an override, + // which is not what the data expresses, so reject it rather than pick a + // winner by document order. + const scope = `${entry.sector}\u0000${entry.geography}`; + const firstSeen = seenScopes.get(scope); + if (firstSeen === undefined) { + seenScopes.set(scope, i); + } else { + errors.push( + `${at} duplicates the scope of /keyFeatures/${field}/${firstSeen}` + + ` (sector "${entry.sector}", geography "${entry.geography}").` + + ` Each scope may carry only one value; to vary a value, vary the scope.`, + ); + } + }); + } + + // #461: a technology must belong to the sector it is attached to. The schema + // types `technologies` as the flat 31-member enum, which is why the generated + // type still reads as "a bit of a random list" -- that breadth is answered here + // rather than in the type. + // + // Closed by default: a sector whose technologies are not defined in + // `timeseriesTaxonomy.ts` accepts only an empty list. The alternative -- passing + // anything through for undefined sectors -- means the next data round populates + // technologies for a new sector and nothing checks them, which is the failure + // this whole check exists to prevent. Rejecting instead makes the missing + // definition impossible to miss, and the message says exactly where to add it. + (pathway.sectors ?? []).forEach((sector, i) => { + if (!sector?.name) return; + const allowed = technologiesForSector(sector.name); + const technologies = sector.technologies ?? []; + if (!allowed) { + if (technologies.length > 0) { + errors.push( + `/sectors/${i}/technologies lists ${quote(technologies)} but no` + + ` technology list is defined for sector "${sector.name}". Add one to` + + ` SECTORS_BY_KEY in src/utils/timeseriesTaxonomy.ts, or use [].`, + ); + } + return; + } + technologies.forEach((technology, t) => { + if (technologyBelongsToSector(technology, sector.name) !== "yes") { + errors.push( + `/sectors/${i}/technologies/${t} "${technology}" is not a technology of` + + ` sector "${sector.name}" ` + + allowedClause(allowed, "technologies"), + ); + } + }); + }); + + // #870: dataAvailability rows. Optional -- authoring is incremental, and a + // pathway without the field is not an invalid pathway. + const availability = pathway.dataAvailability; + if (availability && Array.isArray(availability.byMetric)) { + // Metrics the pathway itself claims to report. Availability for a metric it + // does not report is a typo, not data -- and the likeliest typo of all, + // since the two lists are authored separately. + const reported = new Set(pathway.metric ?? []); + // First index each (metricName, sector, sectorSegment, geography) was seen + // at. NUL-joined so no combination of parts can collide with another; see + // the keyFeatures duplicate check above for the same reasoning. + const seenRows = new Map(); + + availability.byMetric.forEach((row, i) => { + const at = `/dataAvailability/byMetric/${i}`; + + if (!declared.has(row.sector)) { + errors.push( + `${at}/sector "${row.sector}" is not a sector this pathway declares` + + ` (allowed: ${quote(declared)})`, + ); + } + if (!geographies.has(row.geography)) { + errors.push( + `${at}/geography "${row.geography}" is not a geography this pathway` + + ` declares (allowed: ${quote(geographies)})`, + ); + } + + if (!reported.has(row.metricName)) { + errors.push( + `${at}/metricName "${row.metricName}" is not a metric this pathway` + + ` reports (allowed: ${quote(reported)})`, + ); + } + + // Deliberately rejects only a definite "no", unlike the technology and + // segment checks: a sector whose metrics are undefined passes. Those two + // axes have no other constraint, so closing them by default is the only + // thing standing between a typo and production. `metricName` already has + // one -- it must appear in the pathway's own `metric` array, checked just + // above -- so closing this axis too would add no safety while making + // dataAvailability unauthorable for the fourteen sectors whose metrics + // nobody has defined, which is the blockage `UNSEGMENTED` exists to avoid. + if (metricBelongsToSector(row.metricName, row.sector) === "no") { + errors.push( + `${at}/metricName "${row.metricName}" is not a metric of sector` + + ` "${row.sector}" ` + + allowedClause(metricsForSector(row.sector), "metrics"), + ); + } + + if (segmentBelongsToSector(row.sectorSegment, row.sector) !== "yes") { + const defined = segmentsForSector(row.sector); + errors.push( + `${at}/sectorSegment "${row.sectorSegment}" is not a segment of sector` + + ` "${row.sector}" ` + + // UNSEGMENTED is always legal, so it belongs in every allowed list. + allowedClause([...(defined ?? []), UNSEGMENTED], "segments") + + (defined + ? "" + : ` No segments are defined for that sector; add them to` + + ` SECTORS_BY_KEY in src/utils/timeseriesTaxonomy.ts.`), + ); + } + + // Same rule as sectors[].technologies (#461): a breakdown dimension has to + // be a technology the sector actually has. + (row.granularity ?? []).forEach((technology, g) => { + if (technologyBelongsToSector(technology, row.sector) !== "yes") { + errors.push( + `${at}/granularity/${g} "${technology}" is not a technology of` + + ` sector "${row.sector}" ` + + allowedClause(technologiesForSector(row.sector), "technologies"), + ); + } + }); + + // `access` describes the cost of reaching the publisher's copy, so it is + // meaningless for data we serve ourselves and required for data we do not. + const inTool = row.dataFormat === "In tool"; + if (inTool && row.access !== null) { + errors.push( + `${at}/access is "${row.access}" but dataFormat is "In tool" -- we host` + + ` this data, so there is no publisher paywall to describe. Use null.`, + ); + } else if (!inTool && row.access === null) { + errors.push( + `${at}/access is null but dataFormat is "${row.dataFormat}" -- say` + + ` whether reaching it at the publisher is "Free" or "Paywalled".`, + ); + } + + const scope = [ + row.metricName, + row.sector, + row.sectorSegment, + row.geography, + ].join("\u0000"); + const firstSeen = seenRows.get(scope); + if (firstSeen === undefined) { + seenRows.set(scope, i); + } else { + errors.push( + `${at} duplicates the scope of /dataAvailability/byMetric/${firstSeen}` + + ` (metric "${row.metricName}", sector "${row.sector}", segment` + + ` "${row.sectorSegment}", geography "${row.geography}").` + + ` Each combination may describe only one row; the table has one cell` + + ` per column to render it in.`, + ); + } + }); + } + + // dependencies are descriptive and not part of the inheritance chain, but + // #858 still scopes each to a sector, and that sector must be a real one. + // Note this uses `declared`, not `entrySectors`: the schema types this field as + // the plain sector enum, so `cross-sector` is not a legal value here. + (pathway.dependencies ?? []).forEach((dep, i) => { + if (dep?.sector && !declared.has(dep.sector)) { + errors.push( + `/dependencies/${i}/sector "${dep.sector}" is not a sector this pathway` + + ` declares (allowed: ${quote(declared)})`, + ); + } + }); + + return errors; +} diff --git a/testdata/valid/pathwayMetadata_v2_full.json b/testdata/valid/pathwayMetadata_v2_full.json new file mode 100644 index 00000000..379a2b9d --- /dev/null +++ b/testdata/valid/pathwayMetadata_v2_full.json @@ -0,0 +1,246 @@ +{ + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", + "id": "pathway-v2-full", + "name": { + "full": "Full V2 Pathway", + "short": "Full V2" + }, + "publication": { + "title": { + "full": "Example Title", + "short": "Example" + }, + "subtitle": "Exercising every v2 shape", + "author": ["Doe, Jane"], + "publisher": { + "full": "TransitionZero" + }, + "year": 2024, + "month": 6, + "day": 1, + "city": "London", + "license": "CC BY 4.0", + "links": [ + { + "description": "Report", + "url": "https://www.example.com/" + } + ] + }, + "description": "A v2 pathway exercising multi-scope key features.", + "pathwayType": "Normative", + "modelTempIncrease": 1.5, + "modelYearStart": 2020, + "modelYearEnd": 2050, + "modelYearNetzero": 2050, + "geography": { + "global": true, + "regions": { + "South East Asia": ["ID", "TH", "VN"], + "North America": ["CA", "MX", "US"] + }, + "country": ["SG"] + }, + "sectors": [ + { + "name": "Power", + "technologies": ["Solar", "Wind", "Coal"] + }, + { + "name": "Steel", + "technologies": [] + } + ], + "pathwayDescription": "A pathway whose key features vary by sector and geography, used to exercise the scoped-entry shape end to end.", + "metric": [ + "Emissions Intensity", + "Capacity", + "Generation", + "Technology Mix", + "Absolute Emissions" + ], + "keyFeatures": { + "emissionsTrajectory": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Moderate decrease" + }, + { + "sector": "Power", + "geography": "South East Asia", + "value": "Significant decrease" + }, + { + "sector": "Steel", + "geography": "TH", + "value": "Minor decrease" + } + ], + "energyEfficiency": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Moderate improvement" + }, + { + "sector": "Steel", + "geography": "North America", + "value": "No information" + } + ], + "energyDemand": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Minor increase" + } + ], + "electrification": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Significant increase" + }, + { + "sector": "Power", + "geography": "SG", + "value": "Moderate increase" + } + ], + "policyTypes": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": ["Carbon price", "Subsidies"] + }, + { + "sector": "Power", + "geography": "South East Asia", + "value": ["Target technology shares", "Phaseout dates", "Other"] + } + ], + "technologyCostTrend": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Decrease" + } + ], + "emissionsScope": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "CO2e (Kyoto)" + } + ], + "policyAmbition": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "High ambition policies" + }, + { + "sector": "Steel", + "geography": "North America", + "value": "Current/legislated policies" + } + ], + "technologyCostsDetail": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": "Capital costs, O&M, etc." + } + ], + "newTechnologiesIncluded": [ + { + "sector": "cross-sector", + "geography": "Global", + "value": ["CCUS", "DAC", "Green H2/ammonia", "Battery storage"] + }, + { + "sector": "Steel", + "geography": "Global", + "value": ["No new technologies"] + } + ], + "investmentNeeds": [] + }, + "coreDrivers": { + "policies": "Carbon pricing across both covered sectors.", + "emissionsTargets": "Net zero by 2050 with interim 2030 milestones.", + "technologyCosts": null, + "investmentChange": "Investment roughly doubles by 2040.", + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [ + { + "dependency_name": "Infrastructure and logistics", + "dependency_description": "Grid buildout must keep pace with renewable additions.", + "sector": "Power", + "evidence_type": "Quantitative" + }, + { + "dependency_name": "Technology", + "dependency_description": "Hydrogen direct reduction must reach commercial scale.", + "sector": "Steel", + "evidence_type": "Qualitative" + } + ], + "dataAvailability": { + "overall": "The full timeseries file covers all five metrics for Power at annual resolution from 2022 to 2050. Steel is described in the publication only.", + "byMetric": [ + { + "metricName": "Capacity", + "sector": "Power", + "sectorSegment": "Power generation", + "geography": "Global", + "geographyCoverage": "Global", + "timeResolution": "1-year", + "dataFormat": "In tool", + "access": null, + "granularity": ["Solar", "Wind", "Coal"], + "scopeLimitations": "Excludes off-grid generation." + }, + { + "metricName": "Generation", + "sector": "Power", + "sectorSegment": "Storage", + "geography": "South East Asia", + "geographyCoverage": "Regional", + "timeResolution": "5-year", + "dataFormat": "Tabular in publication", + "access": "Free", + "granularity": null, + "scopeLimitations": null + }, + { + "metricName": "Technology Mix", + "sector": "Power", + "sectorSegment": "Transmission & Distribution", + "geography": "SG", + "geographyCoverage": "Country", + "timeResolution": "Single year (2050)", + "dataFormat": "Text in publication", + "access": "Paywalled", + "granularity": null, + "scopeLimitations": "Grid losses only." + }, + { + "metricName": "Absolute Emissions", + "sector": "Steel", + "sectorSegment": "No information", + "geography": "Global", + "geographyCoverage": "Global", + "timeResolution": "No information", + "dataFormat": "Text in publication", + "access": "Free", + "granularity": null, + "scopeLimitations": null + } + ] + } +} diff --git a/testdata/valid/pathwayMetadata_v2_minimal.json b/testdata/valid/pathwayMetadata_v2_minimal.json new file mode 100644 index 00000000..6ea9eb22 --- /dev/null +++ b/testdata/valid/pathwayMetadata_v2_minimal.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://pathways.rmi.org/schema/pathwayMetadata.v2.json", + "id": "pathway-v2-minimal", + "name": { "full": "Minimal V2 Pathway" }, + "publication": { + "title": { "full": "Example Title" }, + "publisher": { "full": "TransitionZero" }, + "year": 2024 + }, + "description": "A minimal v2 pathway file that passes schema validation.", + "pathwayType": "Exploratory", + "geography": { "regions": { "South East Asia": [] } }, + "sectors": [{ "name": "Other", "technologies": [] }], + "pathwayDescription": null, + "metric": ["Capacity"], + "keyFeatures": { + "emissionsTrajectory": [], + "energyEfficiency": [], + "energyDemand": [], + "electrification": [], + "policyTypes": [], + "technologyCostTrend": [], + "emissionsScope": [], + "policyAmbition": [], + "technologyCostsDetail": [], + "newTechnologiesIncluded": [], + "investmentNeeds": [] + }, + "coreDrivers": { + "policies": null, + "emissionsTargets": null, + "technologyCosts": null, + "investmentChange": null, + "macroeconomicDrivers": null, + "behavioralShifts": null, + "otherDrivers": null + }, + "dependencies": [] +} diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo new file mode 100644 index 00000000..a769cbea --- /dev/null +++ b/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.test.tsx","./src/app.tsx","./src/main.tsx","./src/components/additionalinfobox.tsx","./src/components/badge.test.tsx","./src/components/badge.tsx","./src/components/badgearray.test.tsx","./src/components/badgearray.tsx","./src/components/colophon.tsx","./src/components/comparisonkeyfeatures.tsx","./src/components/comparisonplots.test.tsx","./src/components/comparisonplots.tsx","./src/components/comparisonribbon.test.tsx","./src/components/comparisonribbon.tsx","./src/components/donutchart.tsx","./src/components/downloaddataset.test.tsx","./src/components/downloaddataset.tsx","./src/components/dropdownfacetshell.test.tsx","./src/components/dropdownfacetshell.tsx","./src/components/environmentbanner.tsx","./src/components/footer.test.tsx","./src/components/footer.tsx","./src/components/header.test.tsx","./src/components/header.tsx","./src/components/headernav.test.tsx","./src/components/headernav.tsx","./src/components/highlightedtext.test.tsx","./src/components/highlightedtext.tsx","./src/components/keyfeatures.test.tsx","./src/components/keyfeatures.tsx","./src/components/markdown.test.tsx","./src/components/markdown.tsx","./src/components/multilinechart.tsx","./src/components/multiselectdropdown.test.tsx","./src/components/multiselectdropdown.tsx","./src/components/neutralscale.test.tsx","./src/components/neutralscale.tsx","./src/components/normalizedstackedareachart.tsx","./src/components/numericrange.test.tsx","./src/components/numericrange.tsx","./src/components/numericrangedropdown.test.tsx","./src/components/numericrangedropdown.tsx","./src/components/numericrangeslider.tsx","./src/components/pathwaycard.test.tsx","./src/components/pathwaycard.tsx","./src/components/plotselector.test.tsx","./src/components/plotselector.tsx","./src/components/publicationblock.tsx","./src/components/radarchart.tsx","./src/components/regionmemberstooltip.test.tsx","./src/components/regionmemberstooltip.tsx","./src/components/resourcesdropdown.test.tsx","./src/components/resourcesdropdown.tsx","./src/components/searchbox.tsx","./src/components/searchsection.test.tsx","./src/components/searchsection.tsx","./src/components/sentimentscale.test.tsx","./src/components/sentimentscale.tsx","./src/components/stepbystepguide.tsx","./src/components/steppage.tsx","./src/components/steppagenumericrange.tsx","./src/components/steppageremap.tsx","./src/components/textwithtooltip.test.tsx","./src/components/textwithtooltip.tsx","./src/context/comparisoncontext.test.tsx","./src/context/comparisoncontext.tsx","./src/context/filtercontext.test.tsx","./src/context/filtercontext.tsx","./src/data/index.gen.ts","./src/data/pathwaymetadata.ts","./src/pages/comparisonpage.disclaimer.test.tsx","./src/pages/comparisonpage.test.tsx","./src/pages/comparisonpage.tsx","./src/pages/contactpage.tsx","./src/pages/landingpage.test.tsx","./src/pages/landingpage.tsx","./src/pages/legalpage.tsx","./src/pages/pathwaydetailpage.disclaimer.test.tsx","./src/pages/pathwaydetailpage.test.tsx","./src/pages/pathwaydetailpage.tsx","./src/pages/pathwaysearch.test.tsx","./src/pages/pathwaysearch.tsx","./src/pages/resources/resourcesfaqpage.tsx","./src/pages/resources/resourceshowtochooseapathwaypage.tsx","./src/pages/resources/resourcesmethodologypage.test.tsx","./src/pages/resources/resourcesmethodologypage.tsx","./src/pages/resources/resourcesupdatespage.tsx","./src/pages/resources/resourcesusecasespage.tsx","./src/pages/resources/index.ts","./src/schema/pathwaymetadata.v2.test.ts","./src/schema/common/index.ts","./src/test/failonreactwarnings.ts","./src/test/setup.ts","./src/types/index.ts","./src/types/pathwaymetadata.v1.d.ts","./src/types/pathwaymetadata.v2.d.ts","./src/types/pathwaytimeseries.v1.d.ts","./src/types/vite-env.d.ts","./src/types/common/countrycode.v1.d.ts","./src/types/common/emissionsscope.v1.d.ts","./src/types/common/geography.v1.d.ts","./src/types/common/geographyitem.v1.d.ts","./src/types/common/label.v1.d.ts","./src/types/common/metric.v1.d.ts","./src/types/common/publication.v1.d.ts","./src/types/common/scopegeography.v2.d.ts","./src/types/common/scopesector.v2.d.ts","./src/types/common/sector.v1.d.ts","./src/types/common/technology.v1.d.ts","./src/utils/numericrangelimits.ts","./src/utils/absent.test.ts","./src/utils/absent.ts","./src/utils/capitalizewords.tsx","./src/utils/charttooltiplayout.test.ts","./src/utils/charttooltiplayout.ts","./src/utils/facets.test.ts","./src/utils/facets.ts","./src/utils/filterregions.test.ts","./src/utils/filterregions.ts","./src/utils/geographyutils.test.tsx","./src/utils/geographyutils.ts","./src/utils/gettemperaturecolor.ts","./src/utils/keyfeaturescope.test.ts","./src/utils/keyfeaturescope.ts","./src/utils/loaddata.test.tsx","./src/utils/loaddata.ts","./src/utils/normalizeabsent.test.ts","./src/utils/normalizeabsent.ts","./src/utils/searchutils.scopedfacets.test.ts","./src/utils/searchutils.test.tsx","./src/utils/searchutils.ts","./src/utils/sortutils.test.tsx","./src/utils/sortutils.ts","./src/utils/timeseriesavailability.test.ts","./src/utils/timeseriesavailability.ts","./src/utils/timeseriesindex.test.ts","./src/utils/timeseriesindex.ts","./src/utils/timeseriestaxonomy.test.ts","./src/utils/timeseriestaxonomy.ts","./src/utils/tooltiputils.test.tsx","./src/utils/tooltiputils.ts","./src/utils/validatedata.test.tsx","./src/utils/validatedata.ts","./src/utils/validatescopes.test.ts","./src/utils/validatescopes.ts"],"errors":true,"version":"6.0.3"}