Repository: cedar-policy/cedar
Pull request #1417 (2025-01-09 14:08:12)

Minor refactor to partial eval
## Description of changes

Does two minor refactors that occurred to me during https://github.com/cedar-policy/cedar/pull/1391,
one of them in response to @john-h-kastner-aws comment https://github.com/cedar-policy/cedar/pull/1391#discussion_r1907368931

## Checklist for requesting a review

The change in this PR is (choose one, and delete the other options):

- [x] A change "invisible" to users (e.g., documentation, changes to "internal" crates like `cedar-policy-core`, `cedar-validator`, etc.)

I confirm that this PR (choose one, and delete the other options):

- [x] Does not update the CHANGELOG because my change does not significantly impact released code.

I confirm that [`cedar-spec`](https://github.com/cedar-policy/cedar-spec) (choose one, and delete the other options):

- [x] Does not require updates because my change does not impact the Cedar formal model or DRT infrastructure.

I confirm that [`docs.cedarpolicy.com`](https://docs.cedarpolicy.com/) (choose one, and delete the other options):

- [x] Does not require updates because my change does not impact the Cedar language specification.



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 755a9952..bd7689f6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -201,9 +201,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
 
 [[package]]
 name = "bitflags"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
 
 [[package]]
 name = "block-buffer"
@@ -252,18 +252,6 @@ version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
 
-[[package]]
-name = "cargo-lock"
-version = "10.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6469776d007022d505bbcc2be726f5f096174ae76d710ebc609eb3029a45b551"
-dependencies = [
- "semver",
- "serde",
- "toml",
- "url",
-]
-
 [[package]]
 name = "cast"
 version = "0.3.0"
@@ -272,9 +260,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
 [[package]]
 name = "cc"
-version = "1.2.7"
+version = "1.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
+checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
 dependencies = [
  "shlex",
 ]
@@ -305,7 +293,7 @@ dependencies = [
  "serde_json",
  "serde_with",
  "smol_str",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
  "tsify",
  "wasm-bindgen",
 ]
@@ -330,7 +318,7 @@ dependencies = [
  "serde",
  "serde_json",
  "tempfile",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
 ]
 
 [[package]]
@@ -359,7 +347,7 @@ dependencies = [
  "serde_with",
  "smol_str",
  "stacker",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
  "tsify",
  "wasm-bindgen",
 ]
@@ -403,7 +391,7 @@ dependencies = [
  "similar-asserts",
  "smol_str",
  "stacker",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
  "tsify",
  "unicode-security",
  "wasm-bindgen",
@@ -428,19 +416,16 @@ dependencies = [
 name = "cedar-wasm"
 version = "4.3.0"
 dependencies = [
- "cargo-lock",
  "cedar-policy",
  "cedar-policy-core",
  "cedar-policy-formatter",
  "cedar-policy-validator",
  "console_error_panic_hook",
  "cool_asserts",
- "itertools 0.14.0",
  "serde",
  "serde-wasm-bindgen",
  "serde_json",
  "tsify",
- "url",
  "wasm-bindgen",
  "wasm-bindgen-test",
 ]
@@ -499,9 +484,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.23"
+version = "4.5.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
+checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -509,9 +494,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.23"
+version = "4.5.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
+checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
 dependencies = [
  "anstream",
  "anstyle",
@@ -521,9 +506,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.18"
+version = "4.5.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
+checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -754,17 +739,6 @@ dependencies = [
  "crypto-common",
 ]
 
-[[package]]
-name = "displaydoc"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "doc-comment"
 version = "0.3.3"
@@ -837,7 +811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
 dependencies = [
  "libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -867,15 +841,6 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
-[[package]]
-name = "form_urlencoded"
-version = "1.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
-dependencies = [
- "percent-encoding",
-]
-
 [[package]]
 name = "futures-core"
 version = "0.3.31"
@@ -1050,151 +1015,12 @@ dependencies = [
  "cc",
 ]
 
-[[package]]
-name = "icu_collections"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
-dependencies = [
- "displaydoc",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
-dependencies = [
- "displaydoc",
- "litemap",
- "tinystr",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_locid_transform_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
-
-[[package]]
-name = "icu_normalizer"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_normalizer_data",
- "icu_properties",
- "icu_provider",
- "smallvec",
- "utf16_iter",
- "utf8_iter",
- "write16",
- "zerovec",
-]
-
-[[package]]
-name = "icu_normalizer_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
-
-[[package]]
-name = "icu_properties"
-version = "1.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
-dependencies = [
- "displaydoc",
- "icu_collections",
- "icu_locid_transform",
- "icu_properties_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_properties_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
-
-[[package]]
-name = "icu_provider"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_provider_macros",
- "stable_deref_trait",
- "tinystr",
- "writeable",
- "yoke",
- "zerofrom",
- "zerovec",
-]
-
-[[package]]
-name = "icu_provider_macros"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "ident_case"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
-[[package]]
-name = "idna"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
-dependencies = [
- "idna_adapter",
- "smallvec",
- "utf8_iter",
-]
-
-[[package]]
-name = "idna_adapter"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
-dependencies = [
- "icu_normalizer",
- "icu_properties",
-]
-
 [[package]]
 name = "indent_write"
 version = "2.2.0"
@@ -1295,9 +1121,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
 
 [[package]]
 name = "js-sys"
-version = "0.3.76"
+version = "0.3.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
 dependencies = [
  "once_cell",
  "wasm-bindgen",
@@ -1364,15 +1190,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
-
-[[package]]
-name = "litemap"
-version = "0.7.4"
+version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 
 [[package]]
 name = "lock_api"
@@ -1576,12 +1396,6 @@ dependencies = [
  "windows-targets",
 ]
 
-[[package]]
-name = "percent-encoding"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
-
 [[package]]
 name = "petgraph"
 version = "0.6.5"
@@ -1609,9 +1423,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
 
 [[package]]
 name = "pin-utils"
@@ -1711,9 +1525,9 @@ dependencies = [
 
 [[package]]
 name = "prettyplease"
-version = "0.2.25"
+version = "0.2.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
+checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705"
 dependencies = [
  "proc-macro2",
  "syn",
@@ -1730,9 +1544,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.92"
+version = "1.0.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
 dependencies = [
  "unicode-ident",
 ]
@@ -2018,15 +1832,15 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.38.42"
+version = "0.38.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
+checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
 dependencies = [
  "bitflags",
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2062,12 +1876,6 @@ dependencies = [
  "winapi-util",
 ]
 
-[[package]]
-name = "scoped-tls"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
-
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
@@ -2079,9 +1887,6 @@ name = "semver"
 version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
-dependencies = [
- "serde",
-]
 
 [[package]]
 name = "serde"
@@ -2127,9 +1932,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.134"
+version = "1.0.135"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
+checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
 dependencies = [
  "indexmap 2.7.0",
  "itoa",
@@ -2138,15 +1943,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "serde_spanned"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
-dependencies = [
- "serde",
-]
-
 [[package]]
 name = "serde_with"
 version = "3.12.0"
@@ -2244,12 +2040,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "stable_deref_trait"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
-
 [[package]]
 name = "stacker"
 version = "0.1.17"
@@ -2260,7 +2050,7 @@ dependencies = [
  "cfg-if",
  "libc",
  "psm",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2305,26 +2095,15 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
 
 [[package]]
 name = "syn"
-version = "2.0.95"
+version = "2.0.96"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
+checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
 dependencies = [
  "proc-macro2",
  "quote",
  "unicode-ident",
 ]
 
-[[package]]
-name = "synstructure"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "tempfile"
 version = "3.15.0"
@@ -2336,7 +2115,7 @@ dependencies = [
  "getrandom",
  "once_cell",
  "rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2386,11 +2165,11 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.9"
+version = "2.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
 dependencies = [
- "thiserror-impl 2.0.9",
+ "thiserror-impl 2.0.11",
 ]
 
 [[package]]
@@ -2406,9 +2185,9 @@ dependencies = [
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.9"
+version = "2.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2452,16 +2231,6 @@ dependencies = [
  "time-core",
 ]
 
-[[package]]
-name = "tinystr"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
-dependencies = [
- "displaydoc",
- "zerovec",
-]
-
 [[package]]
 name = "tinytemplate"
 version = "1.2.1"
@@ -2487,26 +2256,11 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
-[[package]]
-name = "toml"
-version = "0.8.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
-dependencies = [
- "serde",
- "serde_spanned",
- "toml_datetime",
- "toml_edit",
-]
-
 [[package]]
 name = "toml_datetime"
 version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
-dependencies = [
- "serde",
-]
 
 [[package]]
 name = "toml_edit"
@@ -2515,8 +2269,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
 dependencies = [
  "indexmap 2.7.0",
- "serde",
- "serde_spanned",
  "toml_datetime",
  "winnow",
 ]
@@ -2619,29 +2371,6 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
 
-[[package]]
-name = "url"
-version = "2.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
-dependencies = [
- "form_urlencoded",
- "idna",
- "percent-encoding",
-]
-
-[[package]]
-name = "utf16_iter"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
-
-[[package]]
-name = "utf8_iter"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
-
 [[package]]
 name = "utf8parse"
 version = "0.2.2"
@@ -2681,20 +2410,21 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
 dependencies = [
  "cfg-if",
  "once_cell",
+ "rustversion",
  "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
 dependencies = [
  "bumpalo",
  "log",
@@ -2706,9 +2436,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.49"
+version = "0.4.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -2719,9 +2449,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -2729,9 +2459,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2742,19 +2472,21 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
 
 [[package]]
 name = "wasm-bindgen-test"
-version = "0.3.49"
+version = "0.3.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d44563646eb934577f2772656c7ad5e9c90fac78aa8013d776fcdaf24625d"
+checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
 dependencies = [
  "js-sys",
  "minicov",
- "scoped-tls",
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "wasm-bindgen-test-macro",
@@ -2762,9 +2494,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-test-macro"
-version = "0.3.49"
+version = "0.3.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54171416ce73aa0b9c377b51cc3cb542becee1cd678204812e8392e5b0e4a031"
+checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2773,9 +2505,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.76"
+version = "0.3.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -2787,7 +2519,7 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
 dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2883,49 +2615,13 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winnow"
-version = "0.6.22"
+version = "0.6.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
+checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
 dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "write16"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
-
-[[package]]
-name = "writeable"
-version = "0.5.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
-
-[[package]]
-name = "yoke"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
-dependencies = [
- "serde",
- "stable_deref_trait",
- "yoke-derive",
- "zerofrom",
-]
-
-[[package]]
-name = "yoke-derive"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "synstructure",
-]
-
 [[package]]
 name = "zerocopy"
 version = "0.7.35"
@@ -2946,46 +2642,3 @@ dependencies = [
  "quote",
  "syn",
 ]
-
-[[package]]
-name = "zerofrom"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
-dependencies = [
- "zerofrom-derive",
-]
-
-[[package]]
-name = "zerofrom-derive"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "synstructure",
-]
-
-[[package]]
-name = "zerovec"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
-dependencies = [
- "yoke",
- "zerofrom",
- "zerovec-derive",
-]
-
-[[package]]
-name = "zerovec-derive"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
diff --git a/cedar-policy-cli/CHANGELOG.md b/cedar-policy-cli/CHANGELOG.md
index 4c6d7e32..801bc690 100644
--- a/cedar-policy-cli/CHANGELOG.md
+++ b/cedar-policy-cli/CHANGELOG.md
@@ -5,6 +5,9 @@ Changes to the Cedar language, which are likely to affect users of the CLI, are
 
 ## Unreleased
 
+- Add schema options `schema` and `schema-format` for the `partially-authorize`
+  command (#1416, resolving #1332)
+
 ## 4.2.2
 
 ## 4.2.1
diff --git a/cedar-policy-cli/src/lib.rs b/cedar-policy-cli/src/lib.rs
index 089026f2..43970dd1 100644
--- a/cedar-policy-cli/src/lib.rs
+++ b/cedar-policy-cli/src/lib.rs
@@ -371,7 +371,7 @@ impl RequestArgs {
 
 #[cfg(feature = "partial-eval")]
 impl PartialRequestArgs {
-    fn get_request(&self) -> Result<Request> {
+    fn get_request(&self, schema: Option<&Schema>) -> Result<Request> {
         let mut builder = RequestBuilder::default();
         let qjson: PartialRequestJSON = match self.request_json_file.as_ref() {
             Some(jsonfile) => {
@@ -416,15 +416,16 @@ impl PartialRequestArgs {
             builder = builder.principal(principal);
         }
 
-        if let Some(action) = qjson
+        let action = qjson
             .action
             .map(|s| {
-                s.parse()
+                s.parse::<EntityUid>()
                     .wrap_err_with(|| format!("failed to parse action {s} as entity Uid"))
             })
-            .transpose()?
-        {
-            builder = builder.action(action);
+            .transpose()?;
+
+        if let Some(action_ref) = &action {
+            builder = builder.action(action_ref.clone());
         }
 
         if let Some(resource) = qjson
@@ -441,15 +442,25 @@ impl PartialRequestArgs {
         if let Some(context) = qjson
             .context
             .map(|json| {
-                Context::from_json_value(json.clone(), None)
-                    .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
+                Context::from_json_value(
+                    json.clone(),
+                    schema.and_then(|s| Some((s, action.as_ref()?))),
+                )
+                .wrap_err_with(|| format!("fail to convert context json {json} to Context"))
             })
             .transpose()?
         {
             builder = builder.context(context);
         }
 
-        Ok(builder.build())
+        if let Some(schema) = schema {
+            builder
+                .schema(schema)
+                .build()
+                .wrap_err_with(|| format!("failed to build request with validation"))
+        } else {
+            Ok(builder.build())
+        }
     }
 }
 
@@ -573,6 +584,12 @@ pub struct PartiallyAuthorizeArgs {
     /// Policies args (incorporated by reference)
     #[command(flatten)]
     pub policies: PoliciesArgs,
+    /// Schema args (incorporated by reference)
+    ///
+    /// Used to populate the store with action entities and for schema-based
+    /// parsing of entity hierarchy, if present
+    #[command(flatten)]
+    pub schema: OptionalSchemaArgs,
     /// File containing JSON representation of the Cedar entity hierarchy
     #[arg(long = "entities", value_name = "FILE")]
     pub entities_file: String,
@@ -1320,6 +1337,7 @@ pub fn partial_authorize(args: &PartiallyAuthorizeArgs) -> CedarExitCode {
         &args.request,
         &args.policies,
         &args.entities_file,
+        &args.schema,
         args.timing,
     );
     match ans {
@@ -1563,6 +1581,7 @@ fn execute_partial_request(
     request: &PartialRequestArgs,
     policies: &PoliciesArgs,
     entities_filename: impl AsRef<Path>,
+    schema: &OptionalSchemaArgs,
     compute_duration: bool,
 ) -> Result<PartialResponse, Vec<Report>> {
     let mut errs = vec![];
@@ -1573,14 +1592,21 @@ fn execute_partial_request(
             PolicySet::new()
         }
     };
-    let entities = match load_entities(entities_filename, None) {
+    let schema = match schema.get_schema() {
+        Ok(opt) => opt,
+        Err(e) => {
+            errs.push(e);
+            None
+        }
+    };
+    let entities = match load_entities(entities_filename, schema.as_ref()) {
         Ok(entities) => entities,
         Err(e) => {
             errs.push(e);
             Entities::empty()
         }
     };
-    match request.get_request() {
+    match request.get_request(schema.as_ref()) {
         Ok(request) if errs.is_empty() => {
             let authorizer = Authorizer::new();
             let auth_start = Instant::now();
diff --git a/cedar-policy-core/src/authorizer/partial_response.rs b/cedar-policy-core/src/authorizer/partial_response.rs
index fdf3b500..8059babd 100644
--- a/cedar-policy-core/src/authorizer/partial_response.rs
+++ b/cedar-policy-core/src/authorizer/partial_response.rs
@@ -337,23 +337,11 @@ impl PartialResponse {
     ) -> Result<Request, ConcretizationError> {
         let mut context = self.request.context.clone();
 
-        let principal = if let Some((key, val)) = mapping.get_key_value("principal") {
-            self.request.principal().concretize(key, val)?
-        } else {
-            self.request.principal().clone()
-        };
+        let principal = self.request.principal().concretize("principal", mapping)?;
 
-        let action = if let Some((key, val)) = mapping.get_key_value("action") {
-            self.request.action().concretize(key, val)?
-        } else {
-            self.request.action().clone()
-        };
+        let action = self.request.action.concretize("action", mapping)?;
 
-        let resource = if let Some((key, val)) = mapping.get_key_value("resource") {
-            self.request.resource().concretize(key, val)?
-        } else {
-            self.request.resource().clone()
-        };
+        let resource = self.request.resource.concretize("resource", mapping)?;
 
         if let Some((key, val)) = mapping.get_key_value("context") {
             if let Ok(attrs) = val.get_as_record() {
@@ -406,38 +394,48 @@ impl PartialResponse {
 }
 
 impl EntityUIDEntry {
-    fn concretize(&self, key: &SmolStr, val: &Value) -> Result<Self, ConcretizationError> {
-        if let Ok(uid) = val.get_as_entity() {
-            match self {
-                EntityUIDEntry::Known { euid, .. } => Err(ConcretizationError::VarConfictError {
-                    id: key.to_owned(),
-                    existing_value: euid.as_ref().clone().into(),
-                    given_value: val.clone(),
-                }),
-                EntityUIDEntry::Unknown { ty: None, .. } => {
-                    Ok(EntityUIDEntry::known(uid.clone(), None))
-                }
-                EntityUIDEntry::Unknown {
-                    ty: Some(type_of_unknown),
-                    ..
-                } => {
-                    if type_of_unknown == uid.entity_type() {
-                        Ok(EntityUIDEntry::known(uid.clone(), None))
-                    } else {
-                        Err(ConcretizationError::EntityTypeConfictError {
-                            id: key.to_owned(),
-                            existing_value: type_of_unknown.clone(),
-                            given_value: val.to_owned(),
+    fn concretize(
+        &self,
+        key: &str,
+        mapping: &HashMap<SmolStr, Value>,
+    ) -> Result<Self, ConcretizationError> {
+        if let Some(val) = mapping.get(key) {
+            if let Ok(uid) = val.get_as_entity() {
+                match self {
+                    EntityUIDEntry::Known { euid, .. } => {
+                        Err(ConcretizationError::VarConfictError {
+                            id: key.into(),
+                            existing_value: euid.as_ref().clone().into(),
+                            given_value: val.clone(),
                         })
                     }
+                    EntityUIDEntry::Unknown { ty: None, .. } => {
+                        Ok(EntityUIDEntry::known(uid.clone(), None))
+                    }
+                    EntityUIDEntry::Unknown {
+                        ty: Some(type_of_unknown),
+                        ..
+                    } => {
+                        if type_of_unknown == uid.entity_type() {
+                            Ok(EntityUIDEntry::known(uid.clone(), None))
+                        } else {
+                            Err(ConcretizationError::EntityTypeConfictError {
+                                id: key.into(),
+                                existing_value: type_of_unknown.clone(),
+                                given_value: val.to_owned(),
+                            })
+                        }
+                    }
                 }
+            } else {
+                Err(ConcretizationError::ValueError {
+                    id: key.into(),
+                    expected_type: "entity",
+                    given_value: val.to_owned(),
+                })
             }
         } else {
-            Err(ConcretizationError::ValueError {
-                id: key.to_owned(),
-                expected_type: "entity",
-                given_value: val.to_owned(),
-            })
+            Ok(self.clone())
         }
     }
 }
diff --git a/cedar-policy-core/src/evaluator.rs b/cedar-policy-core/src/evaluator.rs
index 5eb158a5..a369cea6 100644
--- a/cedar-policy-core/src/evaluator.rs
+++ b/cedar-policy-core/src/evaluator.rs
@@ -380,20 +380,20 @@ impl<'e> Evaluator<'e> {
                 // NOTE: There are more precise partial eval opportunities here, esp w/ typed unknowns
                 // Current limitations:
                 //   Operators are not partially evaluated, except in a few 'simple' cases when comparing a concrete value with an unknown of known type
-                //   implemented in short_circuit_typed_residual
+                //   implemented in short_circuit_*
                 let (arg1, arg2) = match (
                     self.partial_interpret(arg1, slots)?,
                     self.partial_interpret(arg2, slots)?,
                 ) {
                     (PartialValue::Value(v1), PartialValue::Value(v2)) => (v1, v2),
                     (PartialValue::Value(v1), PartialValue::Residual(e2)) => {
-                        if let Some(val) = self.short_circuit_typed_residual(&v1, &e2, *op) {
+                        if let Some(val) = self.short_circuit_value_and_residual(&v1, &e2, *op) {
                             return Ok(val);
                         }
                         return Ok(PartialValue::Residual(Expr::binary_app(*op, v1.into(), e2)));
                     }
                     (PartialValue::Residual(e1), PartialValue::Value(v2)) => {
-                        if let Some(val) = self.short_circuit_typed_residual(&v2, &e1, *op) {
+                        if let Some(val) = self.short_circuit_residual_and_value(&e1, &v2, *op) {
                             return Ok(val);
                         }
                         return Ok(PartialValue::Residual(Expr::binary_app(*op, e1, v2.into())));
@@ -922,10 +922,26 @@ impl<'e> Evaluator<'e> {
         }
     }
 
-    /// Evaluate a binary operation between a value and a residual expression. If despite the unknown contained in the residual, concrete result
+    /// Evaluate a binary operation between a residual expression (left) and a value (right). If despite the unknown contained in the residual, concrete result
     /// can be obtained (using the type annotation on the residual), it is returned.
-    /// Since it is not aware which of the inputs is on the left side, and which on the right, it needs to return None for all non-commutative operations.
-    fn short_circuit_typed_residual(
+    fn short_circuit_residual_and_value(
+        &self,
+        e1: &Expr,
+        v2: &Value,
+        op: BinaryOp,
+    ) -> Option<PartialValue> {
+        match op {
+            // Since these operators are commutative, we can use just one order, and have one implementation of the actual logic
+            BinaryOp::Add | BinaryOp::Eq | BinaryOp::Mul | BinaryOp::ContainsAny => {
+                self.short_circuit_value_and_residual(v2, e1, op)
+            }
+            _ => None,
+        }
+    }
+
+    /// Evaluate a binary operation between a value (left) and a residual expression (right). If despite the unknown contained in the residual, concrete result
+    /// can be obtained (using the type annotation on the residual), it is returned.
+    fn short_circuit_value_and_residual(
         &self,
         v1: &Value,
         e2: &Expr,
diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs
index 08a39f93..8add18ab 100644
--- a/cedar-policy-validator/src/entity_manifest.rs
+++ b/cedar-policy-validator/src/entity_manifest.rs
@@ -68,7 +68,7 @@ pub struct EntityManifest {
 // CAUTION: this type is publicly exported in `cedar-policy`.
 // Don't make fields `pub`, don't make breaking changes, and use caution
 // when adding public methods.
-#[doc = include_str!("../../cedar-policy/experimental_warning.md")]
+#[doc = include_str!("../experimental_warning.md")]
 pub type Fields = HashMap<SmolStr, Box<AccessTrie>>;
 
 /// The root of a data path or [`RootAccessTrie`].
@@ -450,7 +450,7 @@ pub fn compute_entity_manifest(
     // now, for each policy we add the data it requires to the manifest
     for policy in policies.policies() {
         // typecheck the policy and get all the request environments
-        let typechecker = Typechecker::new(schema, ValidationMode::Strict, policy.id().clone());
+        let typechecker = Typechecker::new(schema, ValidationMode::Strict);
         let request_envs = typechecker.typecheck_by_request_env(policy.template());
         for (request_env, policy_check) in request_envs {
             let new_primary_slice = match policy_check {
diff --git a/cedar-policy-validator/src/level_validate.rs b/cedar-policy-validator/src/level_validate.rs
index d4e4fbbd..e5c1e388 100644
--- a/cedar-policy-validator/src/level_validate.rs
+++ b/cedar-policy-validator/src/level_validate.rs
@@ -41,12 +41,8 @@ impl Validator {
 
         // Only perform level validation if validation passed.
         if peekable_errors.peek().is_none() {
-            let levels_errors = self.check_entity_deref_level(
-                p,
-                mode,
-                &EntityDerefLevel::from(max_deref_level),
-                p.id(),
-            );
+            let levels_errors =
+                self.check_entity_deref_level(p, mode, &EntityDerefLevel::from(max_deref_level));
             (peekable_errors.chain(levels_errors), warnings)
         } else {
             (peekable_errors.chain(vec![]), warnings)
@@ -60,16 +56,14 @@ impl Validator {
         t: &'a Template,
         mode: ValidationMode,
         max_allowed_level: &EntityDerefLevel,
-        policy_id: &PolicyID,
     ) -> Vec<ValidationError> {
-        let typechecker = Typechecker::new(&self.schema, mode, t.id().clone());
+        let typechecker = Typechecker::new(&self.schema, mode);
         let type_annotated_asts = typechecker.typecheck_by_request_env(t);
         let mut errs = vec![];
         for (_, policy_check) in type_annotated_asts {
             match policy_check {
                 PolicyCheck::Success(e) | PolicyCheck::Irrelevant(_, e) => {
-                    let res =
-                        Self::check_entity_deref_level_helper(&e, max_allowed_level, policy_id);
+                    let res = Self::check_entity_deref_level_helper(&e, max_allowed_level, t.id());
                     if let Some(e) = res.1 {
                         errs.push(ValidationError::EntityDerefLevelViolation(e))
                     }
@@ -302,12 +296,10 @@ mod levels_validation_tests {
         let p = parser::parse_policy(None, src).unwrap();
         set.add_static(p).unwrap();
 
-        let template_name = PolicyID::from_string("policy0");
         let result = validator.check_entity_deref_level(
-            set.get_template(&template_name).unwrap(),
+            set.get_template(&PolicyID::from_string("policy0")).unwrap(),
             ValidationMode::default(),
             &EntityDerefLevel { level: 0 },
-            &template_name,
         );
         assert!(result.is_empty());
     }
@@ -322,12 +314,10 @@ mod levels_validation_tests {
         let p = parser::parse_policy(None, src).unwrap();
         set.add_static(p).unwrap();
 
-        let template_name = PolicyID::from_string("policy0");
         let result = validator.check_entity_deref_level(
-            set.get_template(&template_name).unwrap(),
+            set.get_template(&PolicyID::from_string("policy0")).unwrap(),
             ValidationMode::default(),
             &EntityDerefLevel { level: 0 },
-            &template_name,
         );
         assert!(result.len() == 1);
     }
diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs
index 201e18db..71185110 100644
--- a/cedar-policy-validator/src/lib.rs
+++ b/cedar-policy-validator/src/lib.rs
@@ -255,7 +255,7 @@ impl Validator {
         impl Iterator<Item = ValidationError> + 'a,
         impl Iterator<Item = ValidationWarning> + 'a,
     ) {
-        let typecheck = Typechecker::new(&self.schema, mode, t.id().clone());
+        let typecheck = Typechecker::new(&self.schema, mode);
         let mut errors = HashSet::new();
         let mut warnings = HashSet::new();
         typecheck.typecheck_policy(t, &mut errors, &mut warnings);
diff --git a/cedar-policy-validator/src/schema.rs b/cedar-policy-validator/src/schema.rs
index 715941bc..eb37ddcb 100644
--- a/cedar-policy-validator/src/schema.rs
+++ b/cedar-policy-validator/src/schema.rs
@@ -795,7 +795,7 @@ impl ValidatorSchema {
     /// on `get_entity_types_in`.
     pub(crate) fn get_entity_types_in_set<'a>(
         &'a self,
-        euids: impl IntoIterator<Item = &'a EntityUID> + 'a,
+        euids: impl IntoIterator<Item = &'a EntityUID>,
     ) -> impl Iterator<Item = &'a EntityType> {
         euids.into_iter().flat_map(|e| self.get_entity_types_in(e))
     }
diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs
index 8f00f5a2..e997fc0f 100644
--- a/cedar-policy-validator/src/typecheck.rs
+++ b/cedar-policy-validator/src/typecheck.rs
@@ -24,7 +24,11 @@ mod typecheck_answer;
 use itertools::Itertools;
 pub(crate) use typecheck_answer::TypecheckAnswer;
 
-use std::{borrow::Cow, collections::HashSet, iter::zip};
+use std::{
+    borrow::Cow,
+    collections::{HashMap, HashSet},
+    iter::zip,
+};
 
 use crate::{
     extension_schema::ExtensionFunctionType,
@@ -46,6 +50,7 @@ use cedar_policy_core::{
     },
     expr_builder::ExprBuilder as _,
     extensions::Extensions,
+    parser::Loc,
 };
 
 #[cfg(not(target_arch = "wasm32"))]
@@ -69,23 +74,15 @@ pub struct Typechecker<'a> {
     schema: &'a ValidatorSchema,
     extensions: &'static ExtensionSchemas<'static>,
     mode: ValidationMode,
-    policy_id: PolicyID,
 }
 
 impl<'a> Typechecker<'a> {
-    /// Construct a new typechecker.
-    pub fn new(
-        schema: &'a ValidatorSchema,
-        mode: ValidationMode,
-        policy_id: PolicyID,
-    ) -> Typechecker<'a> {
-        // Set the extensions using `all_available_extension_schemas`.
-        let extensions = ExtensionSchemas::all_available();
+    /// Construct a new typechecker. All extensions are enabled by default.
+    pub fn new(schema: &'a ValidatorSchema, mode: ValidationMode) -> Typechecker<'a> {
         Self {
             schema,
-            extensions,
+            extensions: ExtensionSchemas::all_available(),
             mode,
-            policy_id,
         }
     }
 
@@ -139,15 +136,46 @@ impl<'a> Typechecker<'a> {
     /// typechecks it under every schema-defined request environment. The result contains
     /// these environments and the individual typechecking response for each, in no
     /// particular order.
+    ///
+    /// Callers using this as the toplevel entry point, rather than
+    /// `typecheck_policy()`, will not get `impossible_policy` validation
+    /// warnings.
     pub fn typecheck_by_request_env<'b>(
         &'b self,
         t: &'b Template,
     ) -> Vec<(RequestEnv<'b>, PolicyCheck)> {
-        self.apply_typecheck_fn_by_request_env(t, |request, expr| {
+        let map = self.typecheck_multi_by_request_env([t]);
+        map.into_values()
+            .filter(|(v, _)| !v.is_empty())
+            .next()
+            .unwrap_or_default() // if all the entries have empty vecs, return an empty vec
+            .0
+    }
+
+    /// Same as `typecheck_by_request_env()`, but typechecks multiple policies
+    /// at once and returns all the results indexed by policy ID, more
+    /// efficiently than calling `typecheck_by_request_env()` multiple times.
+    ///
+    /// The `Loc` of each policy is also returned, for error reporting purposes.
+    ///
+    /// Callers using this as the toplevel entry point, rather than
+    /// `typecheck_policy()`, will not get `impossible_policy` validation
+    /// warnings.
+    pub fn typecheck_multi_by_request_env<'b>(
+        &'b self,
+        ts: impl IntoIterator<Item = &'b Template>,
+    ) -> HashMap<PolicyID, (Vec<(RequestEnv<'b>, PolicyCheck)>, Option<Loc>)> {
+        self.apply_typecheck_fn_by_request_env(ts, |request_env, policy_id, expr| {
             let mut type_errors = Vec::new();
+            let single_env_typechecker = SingleEnvTypechecker {
+                schema: self.schema,
+                extensions: self.extensions,
+                mode: self.mode,
+                policy_id,
+                request_env,
+            };
             let empty_prior_capability = CapabilitySet::new();
-            let ty = self.expect_type(
-                request,
+            let ans = single_env_typechecker.expect_type(
                 &empty_prior_capability,
                 expr,
                 Type::primitive_boolean(),
@@ -155,8 +183,8 @@ impl<'a> Typechecker<'a> {
                 |_| None,
             );
 
-            let is_false = ty.contains_type(&Type::singleton_boolean(false));
-            match (is_false, ty.typechecked(), ty.into_typed_expr()) {
+            let is_false = ans.contains_type(&Type::singleton_boolean(false));
+            match (is_false, ans.typechecked(), ans.into_typed_expr()) {
                 (false, true, None) => PolicyCheck::Fail(type_errors),
                 (false, true, Some(e)) => PolicyCheck::Success(e),
                 (false, false, _) => PolicyCheck::Fail(type_errors),
@@ -168,75 +196,49 @@ impl<'a> Typechecker<'a> {
         })
     }
 
-    /// Utility abstracting the common logic for strict and regular typechecking
-    /// by request environment.
+    /// Apply `typecheck_fn` to each policy in every schema-defined request
+    /// environment, and collect all the results.
+    ///
+    /// Results are returned indexed by the policy ID of the policy they belong to.
+    /// Results for a single policy are returned in no particular order.
+    /// The `Loc` of each policy is also returned, for error reporting purposes.
     fn apply_typecheck_fn_by_request_env<'b, F, C>(
         &'b self,
-        t: &'b Template,
+        ts: impl IntoIterator<Item = &'b Template>,
         typecheck_fn: F,
-    ) -> Vec<(RequestEnv<'b>, C)>
+    ) -> HashMap<PolicyID, (Vec<(RequestEnv<'b>, C)>, Option<Loc>)>
     where
-        F: Fn(&RequestEnv<'b>, &Expr) -> C,
+        F: Fn(&RequestEnv<'b>, &PolicyID, &Expr) -> C,
     {
-        let mut result_checks = Vec::new();
+        let mut ret = HashMap::new();
 
-        // Validate each (principal, resource) pair with the substituted policy
-        // for the corresponding action. Implemented as for loop to make it
-        // explicit that `expect_type` will be called for every element of
-        // request_env without short circuiting.
-        let policy_condition = &t.condition();
-        for unlinked_e in self.unlinked_request_envs() {
-            for linked_e in self.link_request_env(&unlinked_e, t) {
-                let check = typecheck_fn(&linked_e, policy_condition);
-                result_checks.push((linked_e, check))
-            }
-        }
-        result_checks
-    }
+        // compute `.condition()` just once for each policy, and cache it here
+        let ts: Vec<(&'b Template, Expr)> = ts.into_iter().map(|t| (t, t.condition())).collect();
 
-    /// Additional entry point for typechecking requests. This method takes a slice
-    /// over policies and typechecks each under every schema-defined request environment.
-    ///
-    /// The result contains these environments in no particular order, but each list of
-    /// policy checks will always match the original order.
-    pub fn multi_typecheck_by_request_env(
-        &self,
-        policy_templates: &[&Template],
-    ) -> Vec<(RequestEnv<'_>, Vec<PolicyCheck>)> {
-        let mut env_checks = Vec::new();
-        for request in self.unlinked_request_envs() {
-            let mut policy_checks = Vec::new();
-            for t in policy_templates.iter() {
-                let condition_expr = t.condition();
-                for linked_env in self.link_request_env(&request, t) {
-                    let mut type_errors = Vec::new();
-                    let empty_prior_capability = CapabilitySet::new();
-                    let ty = self.expect_type(
-                        &linked_env,
-                        &empty_prior_capability,
-                        &condition_expr,
-                        Type::primitive_boolean(),
-                        &mut type_errors,
-                        |_| None,
-                    );
+        // initialize the entry for each `PolicyID` by inserting the appropriate loc
+        for (t, _) in &ts {
+            ret.insert(t.id().clone(), (Vec::new(), t.loc().cloned()));
+        }
 
-                    let is_false = ty.contains_type(&Type::singleton_boolean(false));
-                    match (is_false, ty.typechecked(), ty.into_typed_expr()) {
-                        (false, true, None) => policy_checks.push(PolicyCheck::Fail(type_errors)),
-                        (false, true, Some(e)) => policy_checks.push(PolicyCheck::Success(e)),
-                        (false, false, _) => policy_checks.push(PolicyCheck::Fail(type_errors)),
-                        (true, _, Some(e)) => {
-                            policy_checks.push(PolicyCheck::Irrelevant(type_errors, e))
-                        }
-                        // PANIC SAFETY: `is_false` implies `e` has a type implies `Some(e)`.
-                        #[allow(clippy::unreachable)]
-                        (true, _, None) => unreachable!(),
-                    }
-                }
+        // Validate each (principal, resource) pair with the substituted policy
+        // for the corresponding action.
+        //
+        // this ordering of loop nesting is chosen in order to call
+        // `unlinked_request_envs()` just once for all policies
+        for unlinked_e in self.unlinked_request_envs() {
+            for (t, cond) in &ts {
+                // PANIC SAFETY: already inserted this key above
+                #[allow(clippy::expect_used)]
+                ret.get_mut(t.id())
+                    .expect("already inserted this key above")
+                    .0
+                    .extend(self.link_request_env(&unlinked_e, t).map(|linked_e| {
+                        let check = typecheck_fn(&linked_e, t.id(), cond);
+                        (linked_e, check)
+                    }));
             }
-            env_checks.push((request, policy_checks));
         }
-        env_checks
+        ret
     }
 
     fn unlinked_request_envs(&self) -> impl Iterator<Item = RequestEnv<'_>> + '_ {
@@ -361,16 +363,28 @@ impl<'a> Typechecker<'a> {
             Box::new(std::iter::once(None))
         }
     }
+}
+
+/// Struct which implements typechecking for policies within a single request
+/// env.
+struct SingleEnvTypechecker<'a> {
+    schema: &'a ValidatorSchema,
+    extensions: &'a ExtensionSchemas<'a>,
+    mode: ValidationMode,
+    /// ID of the policy we're typechecking; used for associating any validation
+    /// errors with the correct policy ID
+    policy_id: &'a PolicyID,
+    /// The single env which we're performing typechecking for
+    request_env: &'a RequestEnv<'a>,
+}
 
-    /// This method handles the majority of the work. Given an expression,
-    /// the type for the request, and the prior capability, return the result of
-    /// typechecking the expression, and add any errors encountered into the
-    /// `type_errors` list. The result of typechecking contains the type of the
-    /// expression, any resulting capability after the expression, and a flag
-    /// indicating whether the expression successfully typechecked.
+impl<'a> SingleEnvTypechecker<'a> {
+    /// This method handles the majority of the work. Given an expression, and
+    /// the prior capability, return the result of typechecking the expression
+    /// in the single env this typechecker was constructed for, and add any
+    /// errors encountered into the `type_errors` list.
     fn typecheck<'b>(
         &self,
-        request_env: &RequestEnv<'_>,
         prior_capability: &CapabilitySet<'b>,
         e: &'b Expr,
         type_errors: &mut Vec<ValidationError>,
@@ -384,7 +398,7 @@ impl<'a> Typechecker<'a> {
             // Principal, resource, and context have types defined by
             // the request type.
             ExprKind::Var(Var::Principal) => TypecheckAnswer::success(
-                ExprBuilder::with_data(Some(request_env.principal_type()))
+                ExprBuilder::with_data(Some(self.request_env.principal_type()))
                     .with_same_source_loc(e)
                     .var(Var::Principal),
             ),
@@ -393,7 +407,7 @@ impl<'a> Typechecker<'a> {
             // entity type), so the type of Action is only the entity type name
             // taken from the euid.
             ExprKind::Var(Var::Action) => {
-                match request_env.action_type(self.schema) {
+                match self.request_env.action_type(self.schema) {
                     Some(ty) => TypecheckAnswer::success(
                         ExprBuilder::with_data(Some(ty))
                             .with_same_source_loc(e)
@@ -411,12 +425,12 @@ impl<'a> Typechecker<'a> {
                 }
             }
             ExprKind::Var(Var::Resource) => TypecheckAnswer::success(
-                ExprBuilder::with_data(Some(request_env.resource_type()))
+                ExprBuilder::with_data(Some(self.request_env.resource_type()))
                     .with_same_source_loc(e)
                     .var(Var::Resource),
             ),
             ExprKind::Var(Var::Context) => TypecheckAnswer::success(
-                ExprBuilder::with_data(Some(request_env.context_type()))
+                ExprBuilder::with_data(Some(self.request_env.context_type()))
                     .with_same_source_loc(e)
                     .var(Var::Context),
             ),
@@ -426,13 +440,13 @@ impl<'a> Typechecker<'a> {
             // Template Slots, always has to be an entity.
             ExprKind::Slot(slotid) => TypecheckAnswer::success(
                 ExprBuilder::with_data(Some(if slotid.is_principal() {
-                    request_env
+                    self.request_env
                         .principal_slot()
                         .clone()
                         .map(Type::named_entity_reference)
                         .unwrap_or_else(Type::any_entity_reference)
                 } else if slotid.is_resource() {
-                    request_env
+                    self.request_env
                         .resource_slot()
                         .clone()
                         .map(Type::named_entity_reference)
@@ -499,7 +513,6 @@ impl<'a> Typechecker<'a> {
             } => {
                 // The guard expression must be boolean.
                 let ans_test = self.expect_type(
-                    request_env,
                     prior_capability,
                     test_expr,
                     Type::primitive_boolean(),
@@ -515,7 +528,6 @@ impl<'a> Typechecker<'a> {
                         // by `test`. This enables an attribute access
                         // `principal.foo` after a condition `principal has foo`.
                         let ans_then = self.typecheck(
-                            request_env,
                             &prior_capability.union(&test_capability),
                             then_expr,
                             type_errors,
@@ -534,8 +546,7 @@ impl<'a> Typechecker<'a> {
                         // we know in the `else` branch that the condition
                         // evaluated to `false`. It still can use the original
                         // prior capability.
-                        let ans_else =
-                            self.typecheck(request_env, prior_capability, else_expr, type_errors);
+                        let ans_else = self.typecheck(prior_capability, else_expr, type_errors);
 
                         ans_else.then_typecheck(|typ_else, else_capability| {
                             TypecheckAnswer::success_with_capability(typ_else, else_capability)
@@ -546,14 +557,12 @@ impl<'a> Typechecker<'a> {
                         // prior capability are in their individual cases.
                         let ans_then = self
                             .typecheck(
-                                request_env,
                                 &prior_capability.union(&test_capability),
                                 then_expr,
                                 type_errors,
                             )
                             .map_capability(|capability| capability.union(&test_capability));
-                        let ans_else =
-                            self.typecheck(request_env, prior_capability, else_expr, type_errors);
+                        let ans_else = self.typecheck(prior_capability, else_expr, type_errors);
                         // The type of the if expression is then the least
                         // upper bound of the types of the then and else
                         // branches.  If either of these fails to typecheck, the
@@ -596,7 +605,6 @@ impl<'a> Typechecker<'a> {
 
             ExprKind::And { left, right } => {
                 let ans_left = self.expect_type(
-                    request_env,
                     prior_capability,
                     left,
                     Type::primitive_boolean(),
@@ -625,7 +633,6 @@ impl<'a> Typechecker<'a> {
                             // the right will only be evaluated after the left
                             // evaluated to `true`.
                             let ans_right = self.expect_type(
-                                request_env,
                                 &prior_capability.union(&capability_left),
                                 right,
                                 Type::primitive_boolean(),
@@ -694,7 +701,6 @@ impl<'a> Typechecker<'a> {
             // capability propagation adjusted as necessary.
             ExprKind::Or { left, right } => {
                 let ans_left = self.expect_type(
-                    request_env,
                     prior_capability,
                     left,
                     Type::primitive_boolean(),
@@ -716,7 +722,6 @@ impl<'a> Typechecker<'a> {
                         // left could have evaluated to either `true` or `false`
                         // when the left is evaluated.
                         let ans_right = self.expect_type(
-                            request_env,
                             prior_capability,
                             right,
                             Type::primitive_boolean(),
@@ -782,22 +787,21 @@ impl<'a> Typechecker<'a> {
 
             ExprKind::UnaryApp { .. } => {
                 // INVARIANT: typecheck_unary requires a `UnaryApp`, we've just ensured this
-                self.typecheck_unary(request_env, prior_capability, e, type_errors)
+                self.typecheck_unary(prior_capability, e, type_errors)
             }
             ExprKind::BinaryApp { .. } => {
                 // INVARIANT: typecheck_binary requires a `BinaryApp`, we've just ensured this
-                self.typecheck_binary(request_env, prior_capability, e, type_errors)
+                self.typecheck_binary(prior_capability, e, type_errors)
             }
             ExprKind::ExtensionFunctionApp { .. } => {
                 // INVARIANT: typecheck_extension requires a `ExtensionFunctionApp`, we've just ensured this
-                self.typecheck_extension(request_env, prior_capability, e, type_errors)
+                self.typecheck_extension(prior_capability, e, type_errors)
             }
 
             ExprKind::GetAttr { expr, attr } => {
                 // Accessing an attribute requires either an entity or a record
                 // that has the attribute.
                 let actual = self.expect_one_of_types(
-                    request_env,
                     prior_capability,
                     expr,
                     &[Type::any_entity_reference(), Type::any_record()],
@@ -834,7 +838,7 @@ impl<'a> Typechecker<'a> {
                                             e.source_loc().cloned(),
                                             self.policy_id.clone(),
                                             AttributeAccess::from_expr(
-                                                request_env,
+                                                self.request_env,
                                                 &typ_expr_actual,
                                                 attr.clone(),
                                             ),
@@ -864,7 +868,7 @@ impl<'a> Typechecker<'a> {
                                     e.source_loc().cloned(),
                                     self.policy_id.clone(),
                                     AttributeAccess::from_expr(
-                                        request_env,
+                                        self.request_env,
                                         &typ_expr_actual,
                                         attr.clone(),
                                     ),
@@ -886,7 +890,6 @@ impl<'a> Typechecker<'a> {
             ExprKind::HasAttr { expr, attr } => {
                 // `has` applies to an entity or a record
                 let actual = self.expect_one_of_types(
-                    request_env,
                     prior_capability,
                     expr,
                     &[Type::any_entity_reference(), Type::any_record()],
@@ -996,7 +999,6 @@ impl<'a> Typechecker<'a> {
             ExprKind::Like { expr, pattern } => {
                 // `like` applies to a string
                 let actual = self.expect_type(
-                    request_env,
                     prior_capability,
                     expr,
                     Type::primitive_string(),
@@ -1021,7 +1023,6 @@ impl<'a> Typechecker<'a> {
 
             ExprKind::Is { expr, entity_type } => {
                 self.expect_type(
-                    request_env,
                     prior_capability,
                     expr,
                     Type::any_entity_reference(),
@@ -1095,7 +1096,7 @@ impl<'a> Typechecker<'a> {
             ExprKind::Set(exprs) => {
                 let elem_types = exprs
                     .iter()
-                    .map(|elem| self.typecheck(request_env, prior_capability, elem, type_errors))
+                    .map(|elem| self.typecheck(prior_capability, elem, type_errors))
                     .collect::<Vec<_>>();
 
                 // If we cannot compute a least upper bound for the element
@@ -1144,7 +1145,7 @@ impl<'a> Typechecker<'a> {
                 // Typecheck each attribute initializer expression individually.
                 let record_attr_tys = map
                     .values()
-                    .map(|value| self.typecheck(request_env, prior_capability, value, type_errors));
+                    .map(|value| self.typecheck(prior_capability, value, type_errors));
                 // This will cause the return value to be `TypecheckFail` if any
                 // of the attributes did not typecheck.
                 TypecheckAnswer::sequence_all_then_typecheck(
@@ -1194,7 +1195,6 @@ impl<'a> Typechecker<'a> {
     /// INVARIANT: `bin_expr` must be a `BinaryApp`
     fn typecheck_binary<'b>(
         &self,
-        request_env: &RequestEnv<'_>,
         prior_capability: &CapabilitySet<'b>,
         bin_expr: &'b Expr,
         type_errors: &mut Vec<ValidationError>,
@@ -1209,12 +1209,11 @@ impl<'a> Typechecker<'a> {
             // The arguments to `==` may typecheck with any type, but we will
             // return false if the types are disjoint.
             BinaryOp::Eq => {
-                let lhs_ty = self.typecheck(request_env, prior_capability, arg1, type_errors);
-                let rhs_ty = self.typecheck(request_env, prior_capability, arg2, type_errors);
+                let lhs_ty = self.typecheck(prior_capability, arg1, type_errors);
+                let rhs_ty = self.typecheck(prior_capability, arg2, type_errors);
                 lhs_ty.then_typecheck(|lhs_ty, _| {
                     rhs_ty.then_typecheck(|rhs_ty, _| {
                         let type_of_eq = self.type_of_equality(
-                            request_env,
                             arg1,
                             lhs_ty.data().as_ref(),
                             arg2,
@@ -1250,9 +1249,9 @@ impl<'a> Typechecker<'a> {
                     .map(Type::extension)
                     .chain(std::iter::once(Type::primitive_long()))
                     .collect_vec();
-                let ans_arg1 = self.typecheck(request_env, prior_capability, arg1, type_errors);
+                let ans_arg1 = self.typecheck(prior_capability, arg1, type_errors);
                 ans_arg1.then_typecheck(|expr_ty_arg1, _| {
-                    let ans_arg2 = self.typecheck(request_env, prior_capability, arg2, type_errors);
+                    let ans_arg2 = self.typecheck(prior_capability, arg2, type_errors);
                     ans_arg2.then_typecheck(|expr_ty_arg2, _| {
                         let expr = ExprBuilder::with_data(Some(Type::primitive_boolean()))
                             .with_same_source_loc(bin_expr)
@@ -1377,7 +1376,6 @@ impl<'a> Typechecker<'a> {
                     _ => None,
                 };
                 let ans_arg1 = self.expect_type(
-                    request_env,
                     prior_capability,
                     arg1,
                     Type::primitive_long(),
@@ -1386,7 +1384,6 @@ impl<'a> Typechecker<'a> {
                 );
                 ans_arg1.then_typecheck(|expr_ty_arg1, _| {
                     let ans_arg2 = self.expect_type(
-                        request_env,
                         prior_capability,
                         arg2,
                         Type::primitive_long(),
@@ -1403,19 +1400,11 @@ impl<'a> Typechecker<'a> {
                 })
             }
 
-            BinaryOp::In => self.typecheck_in(
-                request_env,
-                prior_capability,
-                bin_expr,
-                arg1,
-                arg2,
-                type_errors,
-            ),
+            BinaryOp::In => self.typecheck_in(prior_capability, bin_expr, arg1, arg2, type_errors),
 
             BinaryOp::Contains => {
                 // The first argument must be a set.
                 self.expect_type(
-                    request_env,
                     prior_capability,
                     arg1,
                     Type::any_set(),
@@ -1437,7 +1426,7 @@ impl<'a> Typechecker<'a> {
                 )
                 .then_typecheck(|expr_ty_arg1, _| {
                     // The second argument may be any type. We do not care if the element type cannot be in the set.
-                    self.typecheck(request_env, prior_capability, arg2, type_errors)
+                    self.typecheck(prior_capability, arg2, type_errors)
                         .then_typecheck(|expr_ty_arg2, _| {
                             if self.mode.is_strict() {
                                 let annotated_expr =
@@ -1475,7 +1464,6 @@ impl<'a> Typechecker<'a> {
             BinaryOp::ContainsAll | BinaryOp::ContainsAny => {
                 // Both arguments to a `containsAll` or `containsAny` must be sets.
                 self.expect_type(
-                    request_env,
                     prior_capability,
                     arg1,
                     Type::any_set(),
@@ -1496,14 +1484,9 @@ impl<'a> Typechecker<'a> {
                     },
                 )
                 .then_typecheck(|expr_ty_arg1, _| {
-                    self.expect_type(
-                        request_env,
-                        prior_capability,
-                        arg2,
-                        Type::any_set(),
-                        type_errors,
-                        |_| Some(UnexpectedTypeHelp::TryUsingSingleContains),
-                    )
+                    self.expect_type(prior_capability, arg2, Type::any_set(), type_errors, |_| {
+                        Some(UnexpectedTypeHelp::TryUsingSingleContains)
+                    })
                     .then_typecheck(|expr_ty_arg2, _| {
                         if self.mode.is_strict() {
                             let annotated_expr =
@@ -1531,7 +1514,6 @@ impl<'a> Typechecker<'a> {
 
             BinaryOp::HasTag => self
                 .expect_type(
-                    request_env,
                     prior_capability,
                     arg1,
                     Type::any_entity_reference(),
@@ -1540,7 +1522,6 @@ impl<'a> Typechecker<'a> {
                 )
                 .then_typecheck(|expr_ty_arg1, _| {
                     self.expect_type(
-                        request_env,
                         prior_capability,
                         arg2,
                         Type::primitive_string(),
@@ -1604,7 +1585,6 @@ impl<'a> Typechecker<'a> {
 
             BinaryOp::GetTag => {
                 self.expect_type(
-                    request_env,
                     prior_capability,
                     arg1,
                     Type::any_entity_reference(),
@@ -1613,7 +1593,6 @@ impl<'a> Typechecker<'a> {
                 )
                 .then_typecheck(|expr_ty_arg1, _| {
                     self.expect_type(
-                        request_env,
                         prior_capability,
                         arg2,
                         Type::primitive_string(),
@@ -1787,7 +1766,6 @@ impl<'a> Typechecker<'a> {
     /// Get the type for an `==` expression given the input types.
     fn type_of_equality<'b>(
         &self,
-        request_env: &RequestEnv<'_>,
         lhs_expr: &'b Expr,
         lhs_ty: Option<&Type>,
         rhs_expr: &'b Expr,
@@ -1809,8 +1787,8 @@ impl<'a> Typechecker<'a> {
             // the action variable (which is converted into a literal euid
             // according to the binding in the request environment), then we
             // compare the euids on either side.
-            let lhs_euid = Typechecker::euid_from_euid_literal_or_action(request_env, lhs_expr);
-            let rhs_euid = Typechecker::euid_from_euid_literal_or_action(request_env, rhs_expr);
+            let lhs_euid = self.euid_from_euid_literal_or_action(lhs_expr);
+            let rhs_euid = self.euid_from_euid_literal_or_action(rhs_expr);
             if let (Some(lhs_euid), Some(rhs_euid)) = (lhs_euid, rhs_euid) {
                 if lhs_euid == rhs_euid {
                     // If lhs and rhs euid are the same, the equality has type `True`.
@@ -1865,7 +1843,6 @@ impl<'a> Typechecker<'a> {
     /// type false, allowing for short circuiting in `if` and `and` expressions.
     fn typecheck_in<'b>(
         &self,
-        request_env: &RequestEnv<'_>,
         prior_capability: &CapabilitySet<'b>,
         in_expr: &Expr,
         lhs: &'b Expr,
@@ -1875,7 +1852,6 @@ impl<'a> Typechecker<'a> {
         // First, the basic typechecking rules for `in` that apply regardless of
         // the syntactic special cases that follow.
         let ty_lhs = self.expect_type(
-            request_env,
             prior_capability,
             lhs,
             Type::any_entity_reference(),
@@ -1883,7 +1859,6 @@ impl<'a> Typechecker<'a> {
             |_| Some(UnexpectedTypeHelp::TryUsingContains),
         );
         let ty_rhs = self.expect_one_of_types(
-            request_env,
             prior_capability,
             rhs,
             &[
@@ -1916,8 +1891,8 @@ impl<'a> Typechecker<'a> {
                 }
                 let lhs_ty = lhs_expr.data().clone();
                 let rhs_ty = rhs_expr.data().clone();
-                let lhs_as_euid_lit = Typechecker::replace_action_var_with_euid(request_env, lhs);
-                let rhs_as_euid_lit = Typechecker::replace_action_var_with_euid(request_env, rhs);
+                let lhs_as_euid_lit = self.replace_action_var_with_euid(lhs);
+                let rhs_as_euid_lit = self.replace_action_var_with_euid(rhs);
                 match (lhs_as_euid_lit.expr_kind(), rhs_as_euid_lit.expr_kind()) {
                     // var in EntityLiteral. Lookup the descendant types of the entity
                     // literals.  If the principal/resource type is not one of the
@@ -1927,7 +1902,6 @@ impl<'a> Typechecker<'a> {
                         ExprKind::Var(var @ (Var::Principal | Var::Resource)),
                         ExprKind::Lit(Literal::EntityUID(_)),
                     ) => self.type_of_var_in_entity_literals(
-                        request_env,
                         *var,
                         [rhs_as_euid_lit.as_ref()],
                         in_expr,
@@ -1942,7 +1916,6 @@ impl<'a> Typechecker<'a> {
                         ExprKind::Var(var @ (Var::Principal | Var::Resource)),
                         ExprKind::Set(elems),
                     ) => self.type_of_var_in_entity_literals(
-                        request_env,
                         *var,
                         elems.as_ref(),
                         in_expr,
@@ -1960,7 +1933,6 @@ impl<'a> Typechecker<'a> {
                         ExprKind::Lit(Literal::EntityUID(euid0)),
                         ExprKind::Lit(Literal::EntityUID(_)),
                     ) => self.type_of_entity_literal_in_entity_literals(
-                        request_env,
                         euid0,
                         [rhs_as_euid_lit.as_ref()],
                         in_expr,
@@ -1971,7 +1943,6 @@ impl<'a> Typechecker<'a> {
                     // As above, with the same complication, but applied to set of entities.
                     (ExprKind::Lit(Literal::EntityUID(euid)), ExprKind::Set(elems)) => self
                         .type_of_entity_literal_in_entity_literals(
-                            request_env,
                             euid,
                             elems.as_ref(),
                             in_expr,
@@ -2073,14 +2044,8 @@ impl<'a> Typechecker<'a> {
 
     // Given an expression, if that expression is a literal or the `action`
     // variable, return it as an EntityUID. Return `None` otherwise.
-    fn euid_from_euid_literal_or_action(
-        request_env: &RequestEnv<'_>,
-        e: &Expr,
-    ) -> Option<EntityUID> {
-        match Typechecker::replace_action_var_with_euid(request_env, e)
-            .as_ref()
-            .expr_kind()
-        {
+    fn euid_from_euid_literal_or_action(&self, e: &Expr) -> Option<EntityUID> {
+        match self.replace_action_var_with_euid(e).expr_kind() {
             ExprKind::Lit(Literal::EntityUID(e)) => Some((**e).clone()),
             _ => None,
         }
@@ -2090,12 +2055,12 @@ impl<'a> Typechecker<'a> {
     // extracted by `euid_from_uid_literal_or_action`. Return `None` if any
     // cannot be converted.
     fn euids_from_euid_literals_or_action<'b>(
-        request_env: &RequestEnv<'_>,
+        &self,
         exprs: impl IntoIterator<Item = &'b Expr>,
     ) -> Option<Vec<EntityUID>> {
         exprs
             .into_iter()
-            .map(|e| Self::euid_from_euid_literal_or_action(request_env, e))
+            .map(|e| self.euid_from_euid_literal_or_action(e))
             .collect::<Option<Vec<_>>>()
     }
 
@@ -2103,18 +2068,17 @@ impl<'a> Typechecker<'a> {
     /// entity literal or set of entity literals.
     fn type_of_var_in_entity_literals<'b, 'c>(
         &self,
-        request_env: &RequestEnv<'_>,
         lhs_var: Var,
         rhs_elems: impl IntoIterator<Item = &'b Expr>,
         in_expr: &Expr,
         lhs_expr: Expr<Option<Type>>,
         rhs_expr: Expr<Option<Type>>,
     ) -> TypecheckAnswer<'c> {
-        if let Some(rhs) = Typechecker::euids_from_euid_literals_or_action(request_env, rhs_elems) {
+        if let Some(rhs) = self.euids_from_euid_literals_or_action(rhs_elems) {
             let var_etype = if matches!(lhs_var, Var::Principal) {
-                request_env.principal_entity_type()
+                self.request_env.principal_entity_type()
             } else {
-                request_env.resource_entity_type()
+                self.request_env.resource_entity_type()
             };
             match var_etype {
                 None => {
@@ -2140,7 +2104,7 @@ impl<'a> Typechecker<'a> {
                         .all(|e| self.schema.euid_has_known_entity_type(e));
                     if self.schema.is_known_entity_type(var_name) && all_rhs_known {
                         let descendants = self.schema.get_entity_types_in_set(rhs.iter());
-                        Typechecker::entity_in_descendants(
+                        Self::entity_in_descendants(
                             var_name,
                             descendants,
                             in_expr,
@@ -2180,14 +2144,13 @@ impl<'a> Typechecker<'a> {
 
     fn type_of_entity_literal_in_entity_literals<'b, 'c>(
         &self,
-        request_env: &RequestEnv<'_>,
         lhs_euid: &EntityUID,
         rhs_elems: impl IntoIterator<Item = &'b Expr>,
         in_expr: &Expr,
         lhs_expr: Expr<Option<Type>>,
         rhs_expr: Expr<Option<Type>>,
     ) -> TypecheckAnswer<'c> {
-        if let Some(rhs) = Typechecker::euids_from_euid_literals_or_action(request_env, rhs_elems) {
+        if let Some(rhs) = self.euids_from_euid_literals_or_action(rhs_elems) {
             let name = lhs_euid.entity_type();
             // We don't want to apply the action hierarchy check to
             // non-action entities, but now we have a set of entities.
@@ -2250,7 +2213,7 @@ impl<'a> Typechecker<'a> {
     ) -> TypecheckAnswer<'b> {
         let rhs_descendants = self.schema.get_actions_in_set(rhs);
         if let Some(rhs_descendants) = rhs_descendants {
-            Typechecker::entity_in_descendants(lhs, rhs_descendants, in_expr, lhs_expr, rhs_expr)
+            Self::entity_in_descendants(lhs, rhs_descendants, in_expr, lhs_expr, rhs_expr)
         } else {
             let annotated_expr = ExprBuilder::with_data(Some(Type::primitive_boolean()))
                 .with_same_source_loc(in_expr)
@@ -2268,10 +2231,10 @@ impl<'a> Typechecker<'a> {
     // based on the precise EUIDs when they're not actions, so we only look at
     // entity types. The type will be `False` is none of the entities on the rhs
     // have a type which may be an ancestor of the rhs entity type.
-    fn type_of_non_action_in_entities<'b>(
+    fn type_of_non_action_in_entities<'b, 'c>(
         &self,
         lhs: &EntityUID,
-        rhs: &[EntityUID],
+        rhs: &'c [EntityUID],
         in_expr: &Expr,
         lhs_expr: Expr<Option<Type>>,
         rhs_expr: Expr<Option<Type>>,
@@ -2282,13 +2245,7 @@ impl<'a> Typechecker<'a> {
             .all(|e| self.schema.euid_has_known_entity_type(e));
         if self.schema.is_known_entity_type(lhs_ety) && all_rhs_known {
             let rhs_descendants = self.schema.get_entity_types_in_set(rhs.iter());
-            Typechecker::entity_in_descendants(
-                lhs_ety,
-                rhs_descendants,
-                in_expr,
-                lhs_expr,
-                rhs_expr,
-            )
+            Self::entity_in_descendants(lhs_ety, rhs_descendants, in_expr, lhs_expr, rhs_expr)
         } else {
             let annotated_expr = ExprBuilder::with_data(Some(Type::primitive_boolean()))
                 .with_same_source_loc(in_expr)
@@ -2303,9 +2260,9 @@ impl<'a> Typechecker<'a> {
 
     /// Check if the entity is in the list of descendants. Return the singleton
     /// type false if it is not, and boolean otherwise.
-    fn entity_in_descendants<'b, K>(
+    fn entity_in_descendants<'b, 'c, K: 'c>(
         lhs_entity: &K,
-        rhs_descendants: impl IntoIterator<Item = &'a K>,
+        rhs_descendants: impl IntoIterator<Item = &'c K>,
         in_expr: &Expr,
         lhs_expr: Expr<Option<Type>>,
         rhs_expr: Expr<Option<Type>>,
@@ -2330,7 +2287,6 @@ impl<'a> Typechecker<'a> {
     /// INVARIANT: `unary_expr` must be of kind `UnaryApp`
     fn typecheck_unary<'b>(
         &self,
-        request_env: &RequestEnv<'_>,
         prior_capability: &CapabilitySet<'b>,
         unary_expr: &'b Expr,
         type_errors: &mut Vec<ValidationError>,
@@ -2343,7 +2299,6 @@ impl<'a> Typechecker<'a> {
         match op {
             UnaryOp::Not => {
                 let ans_arg = self.expect_type(
-                    request_env,
                     prior_capability,
                     arg,
                     Type::primitive_boolean(),
@@ -2375,7 +2330,6 @@ impl<'a> Typechecker<'a> {
             }
             UnaryOp::Neg => {
                 let ans_arg = self.expect_type(
-                    request_env,
                     prior_capability,
                     arg,
                     Type::primitive_long(),
@@ -2392,7 +2346,6 @@ impl<'a> Typechecker<'a> {
             }
             UnaryOp::IsEmpty => {
                 let ans_arg = self.expect_type(
-                    request_env,
                     prior_capability,
                     arg,
                     Type::any_set(),
@@ -2420,7 +2373,6 @@ impl<'a> Typechecker<'a> {
     /// Return `TypecheckSuccess` with the type otherwise.
     fn expect_one_of_types<'b, F>(
         &self,
-        request_env: &RequestEnv<'_>,
         prior_capability: &CapabilitySet<'b>,
         expr: &'b Expr,
         expected: &[Type],
@@ -2430,7 +2382,7 @@ impl<'a> Typechecker<'a> {
     where
         F: FnOnce(&Type) -> Option<UnexpectedTypeHelp>,
     {
-        let actual = self.typecheck(request_env, prior_capability, expr, type_errors);
+        let actual = self.typecheck(prior_capability, expr, type_errors);
         actual.then_typecheck(|mut typ_actual, capability| match typ_actual.data() {
             Some(actual_ty) => {
                 if !expected.iter().any(|expected_ty| {
@@ -2480,7 +2432,6 @@ impl<'a> Typechecker<'a> {
     /// type.
     fn expect_type<'b, F>(
         &self,
-        request_env: &RequestEnv<'_>,
         prior_capability: &CapabilitySet<'b>,
         expr: &'b Expr,
         expected: Type,
@@ -2491,7 +2442,6 @@ impl<'a> Typechecker<'a> {
         F: FnOnce(&Type) -> Option<UnexpectedTypeHelp>,
     {
         self.expect_one_of_types(
-            request_env,
             prior_capability,
             expr,
             &[expected],
@@ -2543,12 +2493,9 @@ impl<'a> Typechecker<'a> {
     /// If the `maybe_action_var` expression is `Expr::Var(Var::Action)`, return
     /// a expression for the entity uid for the action variable in the request
     /// environment. Otherwise, return the expression unchanged.
-    fn replace_action_var_with_euid(
-        request_env: &RequestEnv<'_>,
-        maybe_action_var: &'a Expr,
-    ) -> Cow<'a, Expr> {
+    fn replace_action_var_with_euid(&self, maybe_action_var: &'a Expr) -> Cow<'a, Expr> {
         match maybe_action_var.expr_kind() {
-            ExprKind::Var(Var::Action) => match request_env.action_entity_uid() {
+            ExprKind::Var(Var::Action) => match self.request_env.action_entity_uid() {
                 Some(action) => Cow::Owned(Expr::val(action.clone())),
                 None => Cow::Borrowed(maybe_action_var),
             },
@@ -2576,7 +2523,6 @@ impl<'a> Typechecker<'a> {
     /// INVARIANT `ext_expr` must be a `ExtensionFunctionApp`
     fn typecheck_extension<'b>(
         &self,
-        request_env: &RequestEnv<'_>,
         prior_capability: &CapabilitySet<'b>,
         ext_expr: &'b Expr,
         type_errors: &mut Vec<ValidationError>,
@@ -2590,7 +2536,7 @@ impl<'a> Typechecker<'a> {
         let typed_arg_exprs = |type_errors: &mut Vec<ValidationError>| {
             args.iter()
                 .map(|arg| {
-                    self.typecheck(request_env, prior_capability, arg, type_errors)
+                    self.typecheck(prior_capability, arg, type_errors)
                         .into_typed_expr()
                 })
                 .collect::<Option<Vec<_>>>()
@@ -2643,14 +2589,7 @@ impl<'a> Typechecker<'a> {
                     }
                 } else {
                     let typechecked_args = zip(args.as_ref(), arg_tys).map(|(arg, ty)| {
-                        self.expect_type(
-                            request_env,
-                            prior_capability,
-                            arg,
-                            ty.clone(),
-                            type_errors,
-                            |_| None,
-                        )
+                        self.expect_type(prior_capability, arg, ty.clone(), type_errors, |_| None)
                     });
                     TypecheckAnswer::sequence_all_then_typecheck(
                         typechecked_args,
diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs
index dc10a721..69df9eed 100644
--- a/cedar-policy/src/api.rs
+++ b/cedar-policy/src/api.rs
@@ -30,6 +30,7 @@ use cedar_policy_validator::entity_manifest;
 pub use cedar_policy_validator::entity_manifest::{
     AccessTrie, EntityManifest, EntityRoot, Fields, RootAccessTrie,
 };
+use cedar_policy_validator::json_schema;
 use cedar_policy_validator::typecheck::{PolicyCheck, Typechecker};
 pub use id::*;
 
@@ -71,7 +72,7 @@ pub(crate) mod version {
         static ref SDK_VERSION: Version = env!("CARGO_PKG_VERSION").parse().unwrap();
         // Cedar language version
         // The patch version field may be unnecessary
-        static ref LANG_VERSION: Version = Version::new(4, 0, 0);
+        static ref LANG_VERSION: Version = Version::new(4, 2, 0);
     }
     /// Get the Cedar SDK Semantic Versioning version
     #[allow(clippy::module_name_repetitions)]
@@ -1397,7 +1398,172 @@ pub struct SchemaFragment {
     lossless: cedar_policy_validator::json_schema::Fragment<cedar_policy_validator::RawName>,
 }
 
+fn get_annotation_by_key(
+    annotations: &est::Annotations,
+    annotation_key: impl AsRef<str>,
+) -> Option<&str> {
+    annotations
+        .0
+        .get(&annotation_key.as_ref().parse().ok()?)
+        .map(|value| annotation_value_to_str_ref(value.as_ref()))
+}
+
+fn annotation_value_to_str_ref(value: Option<&ast::Annotation>) -> &str {
+    value.map_or("", |a| a.as_ref())
+}
+
+fn annotations_to_pairs(annotations: &est::Annotations) -> impl Iterator<Item = (&str, &str)> {
+    annotations
+        .0
+        .iter()
+        .map(|(key, value)| (key.as_ref(), annotation_value_to_str_ref(value.as_ref())))
+}
+
 impl SchemaFragment {
+    /// Get annotations of a non-empty namespace.
+    ///
+    /// We do not allow namespace-level annotations on the empty namespace.
+    ///
+    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
+    pub fn namespace_annotations(
+        &self,
+        namespace: EntityNamespace,
+    ) -> Option<impl Iterator<Item = (&str, &str)>> {
+        self.lossless
+            .0
+            .get(&Some(namespace.0))
+            .map(|ns_def| annotations_to_pairs(&ns_def.annotations))
+    }
+
+    /// Get annotation value of a non-empty namespace by annotation key
+    /// `annotation_key`
+    ///
+    /// We do not allow namespace-level annotations on the empty namespace.
+    ///
+    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
+    /// or `annotation_key` is not a valid annotation key
+    /// or it does not exist
+    pub fn namespace_annotation(
+        &self,
+        namespace: EntityNamespace,
+        annotation_key: impl AsRef<str>,
+    ) -> Option<&str> {
+        let ns = self.lossless.0.get(&Some(namespace.0))?;
+        get_annotation_by_key(&ns.annotations, annotation_key)
+    }
+
+    /// Get annotations of a common type declaration
+    ///
+    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
+    /// `ty` is not a valid common type ID or `ty` is not found in the
+    /// corresponding namespace definition
+    pub fn common_type_annotations(
+        &self,
+        namespace: Option<EntityNamespace>,
+        ty: &str,
+    ) -> Option<impl Iterator<Item = (&str, &str)>> {
+        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
+        let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
+            .ok()?;
+        ns_def
+            .common_types
+            .get(&ty)
+            .map(|ty| annotations_to_pairs(&ty.annotations))
+    }
+
+    /// Get annotation value of a common type declaration by annotation key
+    /// `annotation_key`
+    ///
+    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
+    /// or `ty` is not a valid common type ID
+    /// or `ty` is not found in the corresponding namespace definition
+    /// or `annotation_key` is not a valid annotation key
+    /// or it does not exist
+    pub fn common_type_annotation(
+        &self,
+        namespace: Option<EntityNamespace>,
+        ty: &str,
+        annotation_key: impl AsRef<str>,
+    ) -> Option<&str> {
+        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
+        let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
+            .ok()?;
+        get_annotation_by_key(&ns_def.common_types.get(&ty)?.annotations, annotation_key)
+    }
+
+    /// Get annotations of an entity type declaration
+    ///
+    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
+    /// `ty` is not a valid entity type name or `ty` is not found in the
+    /// corresponding namespace definition
+    pub fn entity_type_annotations(
+        &self,
+        namespace: Option<EntityNamespace>,
+        ty: &str,
+    ) -> Option<impl Iterator<Item = (&str, &str)>> {
+        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
+        let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
+        ns_def
+            .entity_types
+            .get(&ty)
+            .map(|ty| annotations_to_pairs(&ty.annotations))
+    }
+
+    /// Get annotation value of an entity type declaration by annotation key
+    /// `annotation_key`
+    ///
+    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
+    /// or `ty` is not a valid entity type name
+    /// or `ty` is not found in the corresponding namespace definition
+    /// or `annotation_key` is not a valid annotation key
+    /// or it does not exist
+    pub fn entity_type_annotation(
+        &self,
+        namespace: Option<EntityNamespace>,
+        ty: &str,
+        annotation_key: impl AsRef<str>,
+    ) -> Option<&str> {
+        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
+        let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
+        get_annotation_by_key(&ns_def.entity_types.get(&ty)?.annotations, annotation_key)
+    }
+
+    /// Get annotations of an action declaration
+    ///
+    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
+    /// `id` is not found in the corresponding namespace definition
+    pub fn action_annotations(
+        &self,
+        namespace: Option<EntityNamespace>,
+        id: EntityId,
+    ) -> Option<impl Iterator<Item = (&str, &str)>> {
+        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
+        ns_def
+            .actions
+            .get(id.as_ref())
+            .map(|a| annotations_to_pairs(&a.annotations))
+    }
+
+    /// Get annotation value of an action declaration by annotation key
+    /// `annotation_key`
+    ///
+    /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
+    /// or `id` is not found in the corresponding namespace definition
+    /// or `annotation_key` is not a valid annotation key
+    /// or it does not exist
+    pub fn action_annotation(
+        &self,
+        namespace: Option<EntityNamespace>,
+        id: EntityId,
+        annotation_key: impl AsRef<str>,
+    ) -> Option<&str> {
+        let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
+        get_annotation_by_key(
+            &ns_def.actions.get(id.as_ref())?.annotations,
+            annotation_key,
+        )
+    }
+
     /// Extract namespaces defined in this [`SchemaFragment`].
     ///
     /// `None` indicates the empty namespace.
@@ -2502,11 +2668,7 @@ impl RequestEnv {
 // This function is called by [`Template::get_valid_request_envs`] and
 // [`Policy::get_valid_request_envs`]
 fn get_valid_request_envs(ast: &ast::Template, s: &Schema) -> impl Iterator<Item = RequestEnv> {
-    let tc = Typechecker::new(
-        &s.0,
-        cedar_policy_validator::ValidationMode::default(),
-        ast.id().clone(),
-    );
+    let tc = Typechecker::new(&s.0, cedar_policy_validator::ValidationMode::default());
     tc.typecheck_by_request_env(ast)
         .into_iter()
         .filter_map(|(env, pc)| {
diff --git a/cedar-policy/src/ffi/mod.rs b/cedar-policy/src/ffi/mod.rs
index 8a993225..a95e40ef 100644
--- a/cedar-policy/src/ffi/mod.rs
+++ b/cedar-policy/src/ffi/mod.rs
@@ -26,4 +26,6 @@ mod format;
 pub use format::*;
 mod convert;
 pub use convert::*;
+mod version;
+pub use version::*;
 mod tests;
diff --git a/cedar-policy/src/ffi/version.rs b/cedar-policy/src/ffi/version.rs
new file mode 100644
index 00000000..6fee0bac
--- /dev/null
+++ b/cedar-policy/src/ffi/version.rs
@@ -0,0 +1,17 @@
+#[cfg(feature = "wasm")]
+use wasm_bindgen::prelude::wasm_bindgen;
+
+use crate::api;
+
+/// Get language version of Cedar
+#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "getCedarLangVersion"))]
+pub fn get_lang_version() -> String {
+    let version = api::version::get_lang_version();
+    format!("{}.{}", version.major, version.minor)
+}
+
+/// Get SDK version of Cedar
+pub fn get_sdk_version() -> String {
+    let version = api::version::get_sdk_version();
+    format!("{version}")
+}
diff --git a/cedar-wasm/Cargo.toml b/cedar-wasm/Cargo.toml
index 1cf93ac3..2629107d 100644
--- a/cedar-wasm/Cargo.toml
+++ b/cedar-wasm/Cargo.toml
@@ -29,13 +29,8 @@ default = ["console_error_panic_hook"]
 crate-type = ["cdylib", "rlib"]
 
 [dev-dependencies]
-wasm-bindgen-test = "0.3.47"
+wasm-bindgen-test = "0.3.50"
 cool_asserts = "2.0"
 
-[build-dependencies]
-cargo-lock = "10.0.0"
-url = "2.5.4"
-itertools = "0.14.0"
-
 [lints]
 workspace = true
diff --git a/cedar-wasm/build.rs b/cedar-wasm/build.rs
deleted file mode 100644
index 91b8d0cc..00000000
--- a/cedar-wasm/build.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright Cedar Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-use cargo_lock::Lockfile;
-
-/// PANIC SAFETY: This is a build script so it's okay for it to panic. Build should fail if underlying assumptions of this script fail
-#[allow(clippy::expect_used)]
-fn main() {
-    println!("cargo:rerun-if-changed=../Cargo.lock");
-    let lockfile = Lockfile::load("../Cargo.lock").expect("a valid lockfile");
-    let mut iter = lockfile
-        .packages
-        .into_iter()
-        .filter(|p| p.name.as_str() == "cedar-policy");
-    let version = iter
-        .next()
-        .expect("cedar-policy is not found in manifest")
-        .version;
-
-    assert!(iter.next().is_none());
-
-    println!("cargo:rustc-env=CEDAR_VERSION={version}");
-}
diff --git a/cedar-wasm/src/lib.rs b/cedar-wasm/src/lib.rs
index f628820c..2df25352 100644
--- a/cedar-wasm/src/lib.rs
+++ b/cedar-wasm/src/lib.rs
@@ -17,13 +17,20 @@
 use wasm_bindgen::prelude::*;
 mod utils;
 
+use cedar_policy::ffi;
 pub use cedar_policy::ffi::{
     check_parse_context, check_parse_entities, check_parse_policy_set, check_parse_schema, format,
-    is_authorized, policy_to_json, policy_to_text, schema_to_json, schema_to_text, validate,
+    get_lang_version, is_authorized, policy_to_json, policy_to_text, schema_to_json,
+    schema_to_text, validate,
 };
 pub use utils::*;
 
 #[wasm_bindgen(js_name = "getCedarVersion")]
-pub fn get_cedar_version() -> String {
-    std::env!("CEDAR_VERSION").to_string()
+pub fn get_sdk_version_deprecated() -> String {
+    get_sdk_version()
+}
+
+#[wasm_bindgen(js_name = "getCedarSDKVersion")]
+pub fn get_sdk_version() -> String {
+    ffi::get_sdk_version()
 }


Test patch:
diff --git a/cedar-policy-validator/src/typecheck/test/partial.rs b/cedar-policy-validator/src/typecheck/test/partial.rs
index eeda516c..aa1f896c 100644
--- a/cedar-policy-validator/src/typecheck/test/partial.rs
+++ b/cedar-policy-validator/src/typecheck/test/partial.rs
@@ -35,7 +35,7 @@ pub(crate) fn assert_partial_typecheck(
     policy: StaticPolicy,
 ) {
     let schema = schema.try_into().expect("Failed to construct schema.");
-    let typechecker = Typechecker::new(&schema, ValidationMode::Partial, policy.id().clone());
+    let typechecker = Typechecker::new(&schema, ValidationMode::Partial);
     let mut errors: HashSet<ValidationError> = HashSet::new();
     let mut warnings: HashSet<ValidationWarning> = HashSet::new();
     let typechecked = typechecker.typecheck_policy(
@@ -54,7 +54,7 @@ pub(crate) fn assert_partial_typecheck_fails(
     expected_errors: impl IntoIterator<Item = ValidationError>,
 ) {
     let schema = schema.try_into().expect("Failed to construct schema.");
-    let typechecker = Typechecker::new(&schema, ValidationMode::Partial, policy.id().clone());
+    let typechecker = Typechecker::new(&schema, ValidationMode::Partial);
     let mut errors: HashSet<ValidationError> = HashSet::new();
     let mut warnings: HashSet<ValidationWarning> = HashSet::new();
     let typechecked = typechecker.typecheck_policy(
@@ -73,7 +73,7 @@ pub(crate) fn assert_partial_typecheck_warns(
     expected_warnings: impl IntoIterator<Item = ValidationWarning>,
 ) {
     let schema = schema.try_into().expect("Failed to construct schema.");
-    let typechecker = Typechecker::new(&schema, ValidationMode::Partial, policy.id().clone());
+    let typechecker = Typechecker::new(&schema, ValidationMode::Partial);
     let mut errors: HashSet<ValidationError> = HashSet::new();
     let mut warnings: HashSet<ValidationWarning> = HashSet::new();
     let typechecked = typechecker.typecheck_policy(
diff --git a/cedar-policy-validator/src/typecheck/test/policy.rs b/cedar-policy-validator/src/typecheck/test/policy.rs
index 92b7510a..3719be27 100644
--- a/cedar-policy-validator/src/typecheck/test/policy.rs
+++ b/cedar-policy-validator/src/typecheck/test/policy.rs
@@ -135,11 +135,7 @@ fn policy_checked_in_multiple_envs() {
     let schema = simple_schema_file()
         .try_into()
         .expect("Failed to construct schema.");
-    let typechecker = Typechecker::new(
-        &schema,
-        ValidationMode::default(),
-        PolicyID::from_string("0"),
-    );
+    let typechecker = Typechecker::new(&schema, ValidationMode::default());
     let env_checks = typechecker.typecheck_by_request_env(&t);
     // There are 3 possible envs in schema:
     // - User, "view_photo", Photo
@@ -162,11 +158,7 @@ fn policy_checked_in_multiple_envs() {
     let schema = simple_schema_file()
         .try_into()
         .expect("Failed to construct schema.");
-    let typechecker = Typechecker::new(
-        &schema,
-        ValidationMode::default(),
-        PolicyID::from_string("0"),
-    );
+    let typechecker = Typechecker::new(&schema, ValidationMode::default());
     let env_checks = typechecker.typecheck_by_request_env(&t);
     // With the new action, policy is always false for the other two
     assert!(
@@ -1050,7 +1042,7 @@ fn extended_has() {
             y?: {
               z?: Long,
             }
-          }   
+          }
         };
 
         action "action" appliesTo {
diff --git a/cedar-policy-validator/src/typecheck/test/strict.rs b/cedar-policy-validator/src/typecheck/test/strict.rs
index 8ec6178e..3ae934d4 100644
--- a/cedar-policy-validator/src/typecheck/test/strict.rs
+++ b/cedar-policy-validator/src/typecheck/test/strict.rs
@@ -29,11 +29,11 @@ use cedar_policy_core::{
 };
 
 use crate::{
+    extensions::ExtensionSchemas,
     json_schema,
-    typecheck::Typechecker,
+    typecheck::SingleEnvTypechecker,
     types::{AttributeType, CapabilitySet, OpenTag, RequestEnv, Type},
-    validation_errors::LubContext,
-    validation_errors::LubHelp,
+    validation_errors::{LubContext, LubHelp},
     RawName, ValidationError, ValidationMode,
 };
 
@@ -44,21 +44,23 @@ use super::test_utils::{
 #[track_caller] // report the caller's location as the location of the panic, not the location in this function
 fn assert_typechecks_strict(
     schema: json_schema::Fragment<RawName>,
-    env: &RequestEnv<'_>,
+    request_env: &RequestEnv<'_>,
     e: Expr,
     expected_type: Type,
 ) {
     let schema = schema.try_into().expect("Failed to construct schema.");
-    let typechecker = Typechecker::new(&schema, ValidationMode::Strict, expr_id_placeholder());
+    let typechecker = SingleEnvTypechecker {
+        schema: &schema,
+        extensions: ExtensionSchemas::all_available(),
+        mode: ValidationMode::Strict,
+        policy_id: &expr_id_placeholder(),
+        request_env,
+    };
     let mut errs = Vec::new();
-    let answer = typechecker.expect_type(
-        env,
-        &CapabilitySet::new(),
-        &e,
-        expected_type,
-        &mut errs,
-        |_| None,
-    );
+    let answer =
+        typechecker.expect_type(&CapabilitySet::new(), &e, expected_type, &mut errs, |_| {
+            None
+        });
 
     assert_eq!(errs, vec![], "Expression should not contain any errors.");
     assert_matches!(
@@ -70,22 +72,24 @@ fn assert_typechecks_strict(
 #[track_caller] // report the caller's location as the location of the panic, not the location in this function
 fn assert_strict_type_error(
     schema: json_schema::Fragment<RawName>,
-    env: &RequestEnv<'_>,
+    request_env: &RequestEnv<'_>,
     e: Expr,
     expected_type: Type,
     expected_error: ValidationError,
 ) {
     let schema = schema.try_into().expect("Failed to construct schema.");
-    let typechecker = Typechecker::new(&schema, ValidationMode::Strict, expr_id_placeholder());
+    let typechecker = SingleEnvTypechecker {
+        schema: &schema,
+        extensions: ExtensionSchemas::all_available(),
+        mode: ValidationMode::Strict,
+        policy_id: &expr_id_placeholder(),
+        request_env,
+    };
     let mut errs = Vec::new();
-    let answer = typechecker.expect_type(
-        env,
-        &CapabilitySet::new(),
-        &e,
-        expected_type,
-        &mut errs,
-        |_| None,
-    );
+    let answer =
+        typechecker.expect_type(&CapabilitySet::new(), &e, expected_type, &mut errs, |_| {
+            None
+        });
 
     assert_eq!(errs.into_iter().collect::<Vec<_>>(), vec![expected_error]);
     assert_matches!(
@@ -168,10 +172,15 @@ where
 fn strict_typecheck_catches_regular_type_error() {
     with_simple_schema_and_request(|s, q| {
         let schema = s.try_into().expect("Failed to construct schema.");
-        let typechecker = Typechecker::new(&schema, ValidationMode::Strict, expr_id_placeholder());
+        let typechecker = SingleEnvTypechecker {
+            schema: &schema,
+            extensions: ExtensionSchemas::all_available(),
+            mode: ValidationMode::Strict,
+            policy_id: &expr_id_placeholder(),
+            request_env: &q,
+        };
         let mut errs = Vec::new();
         typechecker.expect_type(
-            &q,
             &CapabilitySet::new(),
             &Expr::from_str("1 + false").unwrap(),
             Type::primitive_long(),
diff --git a/cedar-policy-validator/src/typecheck/test/test_utils.rs b/cedar-policy-validator/src/typecheck/test/test_utils.rs
index db94a61b..9768bdbf 100644
--- a/cedar-policy-validator/src/typecheck/test/test_utils.rs
+++ b/cedar-policy-validator/src/typecheck/test/test_utils.rs
@@ -28,7 +28,7 @@ use cedar_policy_core::parser::Loc;
 
 use crate::{
     json_schema,
-    typecheck::{TypecheckAnswer, Typechecker},
+    typecheck::{SingleEnvTypechecker, TypecheckAnswer, Typechecker},
     types::{CapabilitySet, OpenTag, RequestEnv, Type},
     validation_errors::UnexpectedTypeHelp,
     NamespaceDefinitionWithActionAttributes, RawName, ValidationError, ValidationMode,
@@ -82,9 +82,13 @@ impl Type {
 impl Typechecker<'_> {
     /// Typecheck an expression outside the context of a policy. This is
     /// currently only used for testing.
+    ///
+    /// `policy_id`: Policy ID to associate with this `Expr`, for the purposes
+    /// of reporting the policy ID in validation errors
     pub(crate) fn typecheck_expr<'a>(
         &self,
         e: &'a Expr,
+        policy_id: &'a PolicyID,
         unique_type_errors: &mut HashSet<ValidationError>,
     ) -> TypecheckAnswer<'a> {
         // Using bogus entity type names here for testing. They'll be treated as
@@ -102,8 +106,15 @@ impl Typechecker<'_> {
             principal_slot: None,
             resource_slot: None,
         };
+        let typechecker = SingleEnvTypechecker {
+            schema: self.schema,
+            extensions: self.extensions,
+            mode: self.mode,
+            policy_id,
+            request_env: &request_env,
+        };
         let mut type_errors = Vec::new();
-        let ans = self.typecheck(&request_env, &CapabilitySet::new(), e, &mut type_errors);
+        let ans = typechecker.typecheck(&CapabilitySet::new(), e, &mut type_errors);
         unique_type_errors.extend(type_errors);
         ans
     }
@@ -191,7 +202,7 @@ pub(crate) fn assert_policy_typechecks_for_mode(
 ) {
     let policy = policy.into();
     let schema = schema.schema();
-    let mut typechecker = Typechecker::new(&schema, mode, expr_id_placeholder());
+    let mut typechecker = Typechecker::new(&schema, mode);
     let mut type_errors: HashSet<ValidationError> = HashSet::new();
     let mut warnings: HashSet<ValidationWarning> = HashSet::new();
     let typechecked = typechecker.typecheck_policy(&policy, &mut type_errors, &mut warnings);
@@ -261,7 +272,7 @@ pub(crate) fn assert_policy_typecheck_fails_for_mode(
 ) -> HashSet<ValidationError> {
     let policy = policy.into();
     let schema = schema.schema();
-    let typechecker = Typechecker::new(&schema, mode, policy.id().clone());
+    let typechecker = Typechecker::new(&schema, mode);
     let mut type_errors: HashSet<ValidationError> = HashSet::new();
     let mut warnings: HashSet<ValidationWarning> = HashSet::new();
     let typechecked = typechecker.typecheck_policy(&policy, &mut type_errors, &mut warnings);
@@ -279,7 +290,7 @@ pub(crate) fn assert_policy_typecheck_warns_for_mode(
 ) -> HashSet<ValidationWarning> {
     let policy = policy.into();
     let schema = schema.schema();
-    let typechecker = Typechecker::new(&schema, mode, policy.id().clone());
+    let typechecker = Typechecker::new(&schema, mode);
     let mut type_errors: HashSet<ValidationError> = HashSet::new();
     let mut warnings: HashSet<ValidationWarning> = HashSet::new();
     let typechecked = typechecker.typecheck_policy(&policy, &mut type_errors, &mut warnings);
@@ -309,9 +320,10 @@ pub(crate) fn assert_typechecks_for_mode(
     mode: ValidationMode,
 ) {
     let schema = schema.schema();
-    let typechecker = Typechecker::new(&schema, mode, expr_id_placeholder());
+    let typechecker = Typechecker::new(&schema, mode);
     let mut type_errors = HashSet::new();
-    let actual = typechecker.typecheck_expr(&expr, &mut type_errors);
+    let pid = expr_id_placeholder();
+    let actual = typechecker.typecheck_expr(&expr, &pid, &mut type_errors);
     assert_matches!(actual, TypecheckAnswer::TypecheckSuccess { expr_type, .. } => {
         assert_types_eq(typechecker.schema, &expected, &expr_type.into_data().expect("Typechecked expression must have type"));
     });
@@ -352,9 +364,10 @@ pub(crate) fn assert_typecheck_fails_for_mode(
     mode: ValidationMode,
 ) -> HashSet<ValidationError> {
     let schema = schema.schema();
-    let typechecker = Typechecker::new(&schema, mode, expr_id_placeholder());
+    let typechecker = Typechecker::new(&schema, mode);
     let mut type_errors = HashSet::new();
-    let actual = typechecker.typecheck_expr(&expr, &mut type_errors);
+    let pid = expr_id_placeholder();
+    let actual = typechecker.typecheck_expr(&expr, &pid, &mut type_errors);
     assert_matches!(actual, TypecheckAnswer::TypecheckFail { expr_recovery_type } => {
         match (expected_ty.as_ref(), expr_recovery_type.data()) {
             (None, None) => (),
diff --git a/cedar-policy-validator/src/typecheck/test/type_annotation.rs b/cedar-policy-validator/src/typecheck/test/type_annotation.rs
index 19d79894..752205cd 100644
--- a/cedar-policy-validator/src/typecheck/test/type_annotation.rs
+++ b/cedar-policy-validator/src/typecheck/test/type_annotation.rs
@@ -31,13 +31,9 @@ fn assert_expr_has_annotated_ast(e: &Expr, annotated: &Expr<Option<Type>>) {
     let schema = empty_schema_file()
         .try_into()
         .expect("Failed to construct schema.");
-    let typechecker = Typechecker::new(
-        &schema,
-        ValidationMode::default(),
-        PolicyID::from_string("0"),
-    );
+    let typechecker = Typechecker::new(&schema, ValidationMode::default());
     let mut errs = HashSet::new();
-    assert_matches!(typechecker.typecheck_expr(e, &mut errs), crate::typecheck::TypecheckAnswer::TypecheckSuccess { expr_type, .. } => {
+    assert_matches!(typechecker.typecheck_expr(e, &PolicyID::from_string("0"), &mut errs), crate::typecheck::TypecheckAnswer::TypecheckSuccess { expr_type, .. } => {
         assert_eq!(&expr_type, annotated);
     });
 }
@@ -146,10 +142,10 @@ fn expr_typechecks_with_correct_annotation() {
     .unwrap()
     .try_into()
     .expect("Failed to construct schema.");
-    let tc = Typechecker::new(&schema, ValidationMode::default(), expr_id_placeholder());
+    let tc = Typechecker::new(&schema, ValidationMode::default());
     let mut errs = HashSet::new();
     let euid = EntityUID::with_eid_and_type("Foo", "bar").unwrap();
-    match tc.typecheck_expr(&Expr::val(euid.clone()), &mut errs) {
+    match tc.typecheck_expr(&Expr::val(euid.clone()), &expr_id_placeholder(), &mut errs) {
         crate::typecheck::TypecheckAnswer::TypecheckSuccess { expr_type, .. } => {
             assert_eq!(
                 &expr_type,
diff --git a/cedar-policy/src/tests.rs b/cedar-policy/src/tests.rs
index 3e2e1afb..e2892002 100644
--- a/cedar-policy/src/tests.rs
+++ b/cedar-policy/src/tests.rs
@@ -6292,7 +6292,7 @@ mod version_tests {
 
     #[test]
     fn test_lang_version() {
-        assert_eq!(get_lang_version().to_string(), "4.0.0");
+        assert_eq!(get_lang_version().to_string(), "4.2.0");
     }
 }
 
@@ -6524,3 +6524,339 @@ mod reserved_keywords_in_policies {
         }
     }
 }
+
+mod schema_annotations {
+    use std::collections::BTreeMap;
+
+    use cool_asserts::assert_matches;
+
+    use crate::EntityNamespace;
+
+    use super::SchemaFragment;
+
+    #[track_caller]
+    fn example_schema() -> SchemaFragment {
+        SchemaFragment::from_cedarschema_str(
+            r#"
+        @a("a")
+        @b
+        entity A1,A2 {};
+        @c("c")
+        @d
+        type T = Long;
+        @e("e")
+        @f
+        action a1, a2 appliesTo { principal: [A1], resource: [A2] };
+
+        @m("m")
+        @n
+        namespace N {
+          @a("a")
+          @b
+          entity A1,A2 {};
+          @c("c")
+          @d
+          type T = Long;
+          @e("e")
+          @f
+          action a1, a2 appliesTo { principal: [N::A1], resource: [A2] };
+        }
+        "#,
+        )
+        .expect("should be a valid schema fragment")
+        .0
+    }
+
+    #[test]
+    fn namespace_annotations() {
+        let schema = example_schema();
+        let namespace: EntityNamespace = "N".parse().expect("should be a valid name");
+        let annotations = BTreeMap::from_iter(
+            schema
+                .namespace_annotations(namespace.clone())
+                .expect("should get annotations"),
+        );
+        assert_eq!(annotations, BTreeMap::from_iter([("m", "m"), ("n", "")]));
+        assert_matches!(
+            schema
+                .namespace_annotations("NM".parse().unwrap())
+                .map(|_| ()),
+            None
+        );
+
+        assert_matches!(
+            schema.namespace_annotation(namespace.clone(), "m"),
+            Some("m")
+        );
+        assert_matches!(
+            schema.namespace_annotation(namespace.clone(), "n"),
+            Some("")
+        );
+        assert_matches!(schema.namespace_annotation(namespace.clone(), "x"), None);
+        assert_matches!(
+            schema.namespace_annotation("NM".parse().unwrap(), "n"),
+            None
+        );
+    }
+
+    #[test]
+    fn entity_type_annotations() {
+        let schema = example_schema();
+        let annotations = BTreeMap::from_iter([("a", "a"), ("b", "")]);
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .entity_type_annotations(None, "A1")
+                    .expect("should get annotations")
+            )
+        );
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .entity_type_annotations(None, "A2")
+                    .expect("should get annotations")
+            )
+        );
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .entity_type_annotations(
+                        Some("N".parse().expect("should be a valid name")),
+                        "A1"
+                    )
+                    .expect("should get annotations")
+            )
+        );
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .entity_type_annotations(
+                        Some("N".parse().expect("should be a valid name")),
+                        "A2"
+                    )
+                    .expect("should get annotations")
+            )
+        );
+
+        assert_matches!(schema.entity_type_annotation(None, "A1", "b",), Some(""));
+        assert_matches!(schema.entity_type_annotation(None, "A2", "a",), Some("a"));
+        assert_matches!(schema.entity_type_annotation(None, "A3", "a",), None);
+        assert_matches!(schema.entity_type_annotation(None, "A2", "x",), None);
+        assert_matches!(
+            schema.entity_type_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "A1",
+                "b",
+            ),
+            Some("")
+        );
+        assert_matches!(
+            schema.entity_type_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "A2",
+                "a",
+            ),
+            Some("a")
+        );
+        assert_matches!(
+            schema.entity_type_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "A3",
+                "a",
+            ),
+            None
+        );
+        assert_matches!(
+            schema.entity_type_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "A2",
+                "x",
+            ),
+            None
+        );
+        assert_matches!(
+            schema.entity_type_annotation(
+                Some("NM".parse().expect("should be a valid name")),
+                "A1",
+                "b",
+            ),
+            None
+        );
+    }
+
+    #[test]
+    fn common_type_annotations() {
+        let schema = example_schema();
+        let annotations = BTreeMap::from_iter([("c", "c"), ("d", "")]);
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .common_type_annotations(None, "T")
+                    .expect("should get annotations")
+            )
+        );
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .common_type_annotations(
+                        Some("N".parse().expect("should be a valid name")),
+                        "T"
+                    )
+                    .expect("should get annotations")
+            )
+        );
+        assert_matches!(schema.common_type_annotation(None, "T", "c",), Some("c"));
+        assert_matches!(schema.common_type_annotation(None, "T", "d",), Some(""));
+        assert_matches!(schema.common_type_annotation(None, "T1", "c",), None);
+        assert_matches!(schema.common_type_annotation(None, "T", "x",), None);
+
+        assert_matches!(
+            schema.common_type_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "T",
+                "c",
+            ),
+            Some("c")
+        );
+        assert_matches!(
+            schema.common_type_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "T",
+                "d",
+            ),
+            Some("")
+        );
+        assert_matches!(
+            schema.common_type_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "T1",
+                "c",
+            ),
+            None
+        );
+        assert_matches!(
+            schema.common_type_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "T",
+                "x",
+            ),
+            None
+        );
+        assert_matches!(
+            schema.common_type_annotation(
+                Some("NM".parse().expect("should be a valid name")),
+                "T",
+                "c",
+            ),
+            None
+        );
+    }
+
+    #[test]
+    fn action_type_annotations() {
+        let schema = example_schema();
+        let annotations = BTreeMap::from_iter([("e", "e"), ("f", "")]);
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .action_annotations(None, "a1".parse().unwrap(),)
+                    .expect("should get annotations")
+            )
+        );
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .action_annotations(None, "a2".parse().unwrap(),)
+                    .expect("should get annotations")
+            )
+        );
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .action_annotations(
+                        Some("N".parse().expect("should be a valid name")),
+                        "a1".parse().unwrap(),
+                    )
+                    .expect("should get annotations")
+            )
+        );
+        assert_eq!(
+            annotations,
+            BTreeMap::from_iter(
+                schema
+                    .action_annotations(
+                        Some("N".parse().expect("should be a valid name")),
+                        "a2".parse().unwrap(),
+                    )
+                    .expect("should get annotations")
+            )
+        );
+
+        assert_matches!(
+            schema.action_annotation(None, "a1".parse().unwrap(), "e",),
+            Some("e")
+        );
+        assert_matches!(
+            schema.action_annotation(None, "a2".parse().unwrap(), "f",),
+            Some("")
+        );
+        assert_matches!(
+            schema.action_annotation(None, "a3".parse().unwrap(), "e",),
+            None
+        );
+        assert_matches!(
+            schema.action_annotation(None, "a2".parse().unwrap(), "x",),
+            None
+        );
+
+        assert_matches!(
+            schema.action_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "a1".parse().unwrap(),
+                "e",
+            ),
+            Some("e")
+        );
+        assert_matches!(
+            schema.action_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "a2".parse().unwrap(),
+                "f",
+            ),
+            Some("")
+        );
+        assert_matches!(
+            schema.action_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "a3".parse().unwrap(),
+                "e",
+            ),
+            None
+        );
+        assert_matches!(
+            schema.action_annotation(
+                Some("N".parse().expect("should be a valid name")),
+                "a2".parse().unwrap(),
+                "x",
+            ),
+            None
+        );
+        assert_matches!(
+            schema.action_annotation(
+                Some("NM".parse().expect("should be a valid name")),
+                "a1".parse().unwrap(),
+                "e",
+            ),
+            None
+        );
+    }
+}