Repository: mscarey/AuthoritySpoke (language: python)

Issue:
addition operator for Rules
If the outputs of Rule A can function as the inputs of Rule B, the addition operator creates a new Rule with the inputs of Rule A and the outputs of (both A and B?) (but only if one of the Rules is an ALL Rule?).

Should there also be a union operator so that the union of Rules C and D would have all the inputs as well as all the outputs of both C and D (which would probably also require ALL)?

These operators could then be used to check whether Rules contradict each other in the context of background knowledge. See issue #2.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 5a42bb48..7c9785b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,6 @@ example_data/opinions/unused
 notebooks/.ipynb_checkpoints
 notebooks/unused
 notebooks/request_cases.ipynb
-Pipfile.lock
 prof/
 tests/__pycache__/
 tests/.pytest_cache/
diff --git a/Pipfile b/Pipfile
index e83ebcbf..b299587b 100644
--- a/Pipfile
+++ b/Pipfile
@@ -8,11 +8,10 @@ allow_prereleases = true
 
 [packages]
 beautifulsoup4 = ">=4.7.1"
-lxml = "*"
 pint = ">=0.9"
 requests = "*"
 authorityspoke = {path = ".",editable = true}
-ipykernel = "*"
+lxml = "*"
 
 [dev-packages]
 black = "*"
@@ -26,11 +25,12 @@ rope = "*"
 sphinx = "*"
 sphinx-autodoc-typehints = "*"
 pipenv-to-requirements = "*"
-wily = "*"
 coveralls = "*"
 setuptools = "*"
 wheel = "*"
 twine = "*"
+ipykernel = "*"
+doc8 = "*"
 
 [requires]
 python_version = "3.7.3"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index f26dc22e..00000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,997 +0,0 @@
-{
-    "_meta": {
-        "hash": {
-            "sha256": "edd9318b7c1d58c029de85d30178ece582c08a14467b1d901a3ea31553b3a51d"
-        },
-        "pipfile-spec": 6,
-        "requires": {
-            "python_version": "3.7.3"
-        },
-        "sources": [
-            {
-                "name": "pypi",
-                "url": "https://pypi.org/simple",
-                "verify_ssl": true
-            }
-        ]
-    },
-    "default": {
-        "appnope": {
-            "hashes": [
-                "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0",
-                "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"
-            ],
-            "markers": "sys_platform == 'darwin'",
-            "version": "==0.1.0"
-        },
-        "authorityspoke": {
-            "editable": true,
-            "path": "."
-        },
-        "backcall": {
-            "hashes": [
-                "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
-                "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"
-            ],
-            "version": "==0.1.0"
-        },
-        "beautifulsoup4": {
-            "hashes": [
-                "sha256:034740f6cb549b4e932ae1ab975581e6103ac8f942200a0e9759065984391858",
-                "sha256:945065979fb8529dd2f37dbb58f00b661bdbcbebf954f93b32fdf5263ef35348",
-                "sha256:ba6d5c59906a85ac23dadfe5c88deaf3e179ef565f4898671253e50a78680718"
-            ],
-            "index": "pypi",
-            "version": "==4.7.1"
-        },
-        "certifi": {
-            "hashes": [
-                "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
-                "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
-            ],
-            "version": "==2019.3.9"
-        },
-        "chardet": {
-            "hashes": [
-                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
-                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
-            ],
-            "version": "==3.0.4"
-        },
-        "decorator": {
-            "hashes": [
-                "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
-                "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
-            ],
-            "version": "==4.4.0"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
-                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
-            ],
-            "version": "==2.8"
-        },
-        "ipykernel": {
-            "hashes": [
-                "sha256:346189536b88859937b5f4848a6fd85d1ad0729f01724a411de5cae9b618819c",
-                "sha256:f0e962052718068ad3b1d8bcc703794660858f58803c3798628817f492a8769c"
-            ],
-            "index": "pypi",
-            "version": "==5.1.1"
-        },
-        "ipython": {
-            "hashes": [
-                "sha256:54c5a8aa1eadd269ac210b96923688ccf01ebb2d0f21c18c3c717909583579a8",
-                "sha256:e840810029224b56cd0d9e7719dc3b39cf84d577f8ac686547c8ba7a06eeab26"
-            ],
-            "version": "==7.5.0"
-        },
-        "ipython-genutils": {
-            "hashes": [
-                "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
-                "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
-            ],
-            "version": "==0.2.0"
-        },
-        "jedi": {
-            "hashes": [
-                "sha256:2bb0603e3506f708e792c7f4ad8fc2a7a9d9c2d292a358fbbd58da531695595b",
-                "sha256:2c6bcd9545c7d6440951b12b44d373479bf18123a401a52025cf98563fbd826c"
-            ],
-            "version": "==0.13.3"
-        },
-        "jupyter-client": {
-            "hashes": [
-                "sha256:b5f9cb06105c1d2d30719db5ffb3ea67da60919fb68deaefa583deccd8813551",
-                "sha256:c44411eb1463ed77548bc2d5ec0d744c9b81c4a542d9637c7a52824e2121b987"
-            ],
-            "version": "==5.2.4"
-        },
-        "jupyter-core": {
-            "hashes": [
-                "sha256:927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970",
-                "sha256:ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7"
-            ],
-            "version": "==4.4.0"
-        },
-        "lxml": {
-            "hashes": [
-                "sha256:03984196d00670b2ab14ae0ea83d5cc0cfa4f5a42558afa9ab5fa745995328f5",
-                "sha256:0815b0c9f897468de6a386dc15917a0becf48cc92425613aa8bbfc7f0f82951f",
-                "sha256:175f3825f075cf02d15099eb52658457cf0ff103dcf11512b5d2583e1d40f58b",
-                "sha256:30e14c62d88d1e01a26936ecd1c6e784d4afc9aa002bba4321c5897937112616",
-                "sha256:3210da6f36cf4b835ff1be853962b22cc354d506f493b67a4303c88bbb40d57b",
-                "sha256:40f60819fbd5bad6e191ba1329bfafa09ab7f3f174b3d034d413ef5266963294",
-                "sha256:43b26a865a61549919f8a42e094dfdb62847113cf776d84bd6b60e4e3fc20ea3",
-                "sha256:4a03dd682f8e35a10234904e0b9508d705ff98cf962c5851ed052e9340df3d90",
-                "sha256:62f382cddf3d2e52cf266e161aa522d54fd624b8cc567bc18f573d9d50d40e8e",
-                "sha256:7b98f0325be8450da70aa4a796c4f06852949fe031878b4aa1d6c417a412f314",
-                "sha256:846a0739e595871041385d86d12af4b6999f921359b38affb99cdd6b54219a8f",
-                "sha256:a3080470559938a09a5d0ec558c005282e99ac77bf8211fb7b9a5c66390acd8d",
-                "sha256:ad841b78a476623955da270ab8d207c3c694aa5eba71f4792f65926dc46c6ee8",
-                "sha256:afdd75d9735e44c639ffd6258ce04a2de3b208f148072c02478162d0944d9da3",
-                "sha256:b4fbf9b552faff54742bcd0791ab1da5863363fb19047e68f6592be1ac2dab33",
-                "sha256:b90c4e32d6ec089d3fa3518436bdf5ce4d902a0787dbd9bb09f37afe8b994317",
-                "sha256:b91cfe4438c741aeff662d413fd2808ac901cc6229c838236840d11de4586d63",
-                "sha256:bdb0593a42070b0a5f138b79b872289ee73c8e25b3f0bea6564e795b55b6bcdd",
-                "sha256:c4e4bca2bb68ce22320297dfa1a7bf070a5b20bcbaec4ee023f83d2f6e76496f",
-                "sha256:cec4ab14af9eae8501be3266ff50c3c2aecc017ba1e86c160209bb4f0423df6a",
-                "sha256:e83b4b2bf029f5104bc1227dbb7bf5ace6fd8fabaebffcd4f8106fafc69fc45f",
-                "sha256:e995b3734a46d41ae60b6097f7c51ba9958648c6d1e0935b7e0ee446ee4abe22",
-                "sha256:f679d93dec7f7210575c85379a31322df4c46496f184ef650d3aba1484b38a2d",
-                "sha256:fd213bb5166e46974f113c8228daaef1732abc47cb561ce9c4c8eaed4bd3b09b",
-                "sha256:fdcb57b906dbc1f80666e6290e794ab8fb959a2e17aa5aee1758a85d1da4533f",
-                "sha256:ff424b01d090ffe1947ec7432b07f536912e0300458f9a7f48ea217dd8362b86"
-            ],
-            "index": "pypi",
-            "version": "==4.3.3"
-        },
-        "parso": {
-            "hashes": [
-                "sha256:17cc2d7a945eb42c3569d4564cdf49bde221bc2b552af3eca9c1aad517dcdd33",
-                "sha256:2e9574cb12e7112a87253e14e2c380ce312060269d04bd018478a3c92ea9a376"
-            ],
-            "version": "==0.4.0"
-        },
-        "pexpect": {
-            "hashes": [
-                "sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1",
-                "sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"
-            ],
-            "markers": "sys_platform != 'win32'",
-            "version": "==4.7.0"
-        },
-        "pickleshare": {
-            "hashes": [
-                "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
-                "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
-            ],
-            "version": "==0.7.5"
-        },
-        "pint": {
-            "hashes": [
-                "sha256:32d8a9a9d63f4f81194c0014b3b742679dce81a26d45127d9810a68a561fe4e2",
-                "sha256:7ece3f639ad58073ce49982b022d464014e6d91d0b3eaa89c8e8ea9c38e32659"
-            ],
-            "index": "pypi",
-            "version": "==0.9"
-        },
-        "prompt-toolkit": {
-            "hashes": [
-                "sha256:11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463780",
-                "sha256:2519ad1d8038fd5fc8e770362237ad0364d16a7650fb5724af6997ed5515e3c1",
-                "sha256:977c6583ae813a37dc1c2e1b715892461fcbdaa57f6fc62f33a528c4886c8f55"
-            ],
-            "version": "==2.0.9"
-        },
-        "ptyprocess": {
-            "hashes": [
-                "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
-                "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
-            ],
-            "version": "==0.6.0"
-        },
-        "pygments": {
-            "hashes": [
-                "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
-                "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
-            ],
-            "version": "==2.4.2"
-        },
-        "python-dateutil": {
-            "hashes": [
-                "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
-                "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
-            ],
-            "version": "==2.8.0"
-        },
-        "pyzmq": {
-            "hashes": [
-                "sha256:1651e52ed91f0736afd6d94ef9f3259b5534ce8beddb054f3d5ca989c4ef7c4f",
-                "sha256:5ccb9b3d4cd20c000a9b75689d5add8cd3bce67fcbd0f8ae1b59345247d803af",
-                "sha256:5e120c4cd3872e332fb35d255ad5998ebcee32ace4387b1b337416b6b90436c7",
-                "sha256:5e2a3707c69a7281a9957f83718815fd74698cba31f6d69f9ed359921f662221",
-                "sha256:63d51add9af8d0442dc90f916baf98fdc04e3b0a32afec4bfc83f8d85e72959f",
-                "sha256:65c5a0bdc49e20f7d6b03a661f71e2fda7a99c51270cafe71598146d09810d0d",
-                "sha256:66828fabe911aa545d919028441a585edb7c9c77969a5fea6722ef6e6ece38ab",
-                "sha256:7d79427e82d9dad6e9b47c0b3e7ae5f9d489b1601e3a36ea629bb49501a4daf3",
-                "sha256:824ee5d3078c4eae737ffc500fbf32f2b14e6ec89b26b435b7834febd70120cf",
-                "sha256:89dc0a83cccec19ff3c62c091e43e66e0183d1e6b4658c16ee4e659518131494",
-                "sha256:8b319805f6f7c907b101c864c3ca6cefc9db8ce0791356f180b1b644c7347e4c",
-                "sha256:90facfb379ab47f94b19519c1ecc8ec8d10813b69d9c163117944948bdec5d15",
-                "sha256:a0a178c7420021fc0730180a914a4b4b3092ce9696ceb8e72d0f60f8ce1655dd",
-                "sha256:a7a89591ae315baccb8072f216614b3e59aed7385aef4393a6c741783d6ee9cf",
-                "sha256:ba2578f0ae582452c02ed9fac2dc477b08e80ce05d2c0885becf5fff6651ccb0",
-                "sha256:c69b0055c55702f5b0b6b354133e8325b9a56dbc80e1be2d240bead253fb9825",
-                "sha256:ca434e1858fe222380221ddeb81e86f45522773344c9da63c311d17161df5e06",
-                "sha256:d4b8ecfc3d92f114f04d5c40f60a65e5196198b827503341521dda12d8b14939",
-                "sha256:d706025c47b09a54f005953ebe206f6d07a22516776faa4f509aaff681cc5468",
-                "sha256:d8f27e958f8a2c0c8ffd4d8855c3ce8ac3fa1e105f0491ce31729aa2b3229740",
-                "sha256:dbd264298f76b9060ce537008eb989317ca787c857e23cbd1b3ddf89f190a9b1",
-                "sha256:e926d66f0df8fdbf03ba20583af0f215e475c667fb033d45fd031c66c63e34c9",
-                "sha256:efc3bd48237f973a749f7312f68062f1b4ca5c2032a0673ca3ea8e46aa77187b",
-                "sha256:f59bc782228777cbfe04555707a9c56d269c787ed25d6d28ed9d0fbb41cb1ad2",
-                "sha256:f8da5322f4ff5f667a0d5a27e871b560c6637153c81e318b35cb012b2a98835c"
-            ],
-            "version": "==18.0.1"
-        },
-        "requests": {
-            "hashes": [
-                "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
-                "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
-            ],
-            "index": "pypi",
-            "version": "==2.22.0"
-        },
-        "six": {
-            "hashes": [
-                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
-                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
-            ],
-            "version": "==1.12.0"
-        },
-        "soupsieve": {
-            "hashes": [
-                "sha256:6898e82ecb03772a0d82bd0d0a10c0d6dcc342f77e0701d0ec4a8271be465ece",
-                "sha256:b20eff5e564529711544066d7dc0f7661df41232ae263619dede5059799cdfca"
-            ],
-            "version": "==1.9.1"
-        },
-        "tornado": {
-            "hashes": [
-                "sha256:1174dcb84d08887b55defb2cda1986faeeea715fff189ef3dc44cce99f5fca6b",
-                "sha256:2613fab506bd2aedb3722c8c64c17f8f74f4070afed6eea17f20b2115e445aec",
-                "sha256:44b82bc1146a24e5b9853d04c142576b4e8fa7a92f2e30bc364a85d1f75c4de2",
-                "sha256:457fcbee4df737d2defc181b9073758d73f54a6cfc1f280533ff48831b39f4a8",
-                "sha256:49603e1a6e24104961497ad0c07c799aec1caac7400a6762b687e74c8206677d",
-                "sha256:8c2f40b99a8153893793559919a355d7b74649a11e59f411b0b0a1793e160bc0",
-                "sha256:e1d897889c3b5a829426b7d52828fb37b28bc181cd598624e65c8be40ee3f7fa"
-            ],
-            "version": "==6.0.2"
-        },
-        "traitlets": {
-            "hashes": [
-                "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
-                "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
-            ],
-            "version": "==4.3.2"
-        },
-        "urllib3": {
-            "hashes": [
-                "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
-                "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
-            ],
-            "version": "==1.25.3"
-        },
-        "wcwidth": {
-            "hashes": [
-                "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
-                "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
-            ],
-            "version": "==0.1.7"
-        }
-    },
-    "develop": {
-        "alabaster": {
-            "hashes": [
-                "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
-                "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
-            ],
-            "version": "==0.7.12"
-        },
-        "appdirs": {
-            "hashes": [
-                "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
-                "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
-            ],
-            "version": "==1.4.3"
-        },
-        "astroid": {
-            "hashes": [
-                "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4",
-                "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4"
-            ],
-            "version": "==2.2.5"
-        },
-        "atomicwrites": {
-            "hashes": [
-                "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
-                "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
-            ],
-            "version": "==1.3.0"
-        },
-        "attrs": {
-            "hashes": [
-                "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
-                "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
-            ],
-            "version": "==19.1.0"
-        },
-        "babel": {
-            "hashes": [
-                "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
-                "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
-            ],
-            "version": "==2.7.0"
-        },
-        "black": {
-            "hashes": [
-                "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
-                "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
-            ],
-            "index": "pypi",
-            "version": "==19.3b0"
-        },
-        "certifi": {
-            "hashes": [
-                "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
-                "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
-            ],
-            "version": "==2019.3.9"
-        },
-        "chardet": {
-            "hashes": [
-                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
-                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
-            ],
-            "version": "==3.0.4"
-        },
-        "click": {
-            "hashes": [
-                "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
-                "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
-            ],
-            "version": "==7.0"
-        },
-        "colorama": {
-            "hashes": [
-                "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
-                "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
-            ],
-            "version": "==0.4.1"
-        },
-        "colorlog": {
-            "hashes": [
-                "sha256:3cf31b25cbc8f86ec01fef582ef3b840950dea414084ed19ab922c8b493f9b42",
-                "sha256:450f52ea2a2b6ebb308f034ea9a9b15cea51e65650593dca1da3eb792e4e4981"
-            ],
-            "version": "==4.0.2"
-        },
-        "coverage": {
-            "hashes": [
-                "sha256:0402b1822d513d0231589494bceddb067d20581f5083598c451b56c684b0e5d6",
-                "sha256:0644e28e8aea9d9d563607ee8b7071b07dd57a4a3de11f8684cd33c51c0d1b93",
-                "sha256:0874a283686803884ec0665018881130604956dbaa344f2539c46d82cbe29eda",
-                "sha256:0988c3837df4bc371189bb3425d5232cf150055452034c232dda9cbe04f9c38e",
-                "sha256:20bc3205b3100956bb72293fabb97f0ed972c81fed10b3251c90c70dcb0599ab",
-                "sha256:2cc9142a3367e74eb6b19d58c53ebb1dfd7336b91cdcc91a6a2888bf8c7af984",
-                "sha256:3ae9a0a59b058ce0761c3bd2c2d66ecb2ee2b8ac592620184370577f7a546fb3",
-                "sha256:3b2e30b835df58cb973f478d09f3d82e90c98c8e5059acc245a8e4607e023801",
-                "sha256:401e9b04894eb1498c639c6623ee78a646990ce5f095248e2440968aafd6e90e",
-                "sha256:41ec5812d5decdaa72708be3018e7443e90def4b5a71294236a4df192cf9eab9",
-                "sha256:475769b638a055e75b3d3219e054fe2a023c0b077ff15bff6c95aba7e93e6cac",
-                "sha256:61424f4e2e82c4129a4ba71e10ebacb32a9ecd6f80de2cd05bdead6ba75ed736",
-                "sha256:811969904d4dd0bee7d958898be8d9d75cef672d9b7e7db819dfeac3d20d2d0c",
-                "sha256:86224bb99abfd672bf2f9fcecad5e8d7a3fa94f7f71513f2210460a0350307cd",
-                "sha256:9a238a20a3af00665f8381f7e53e9c606f9bb652d2423f6b822f6cb790d887e8",
-                "sha256:a23b3fbc14d4e6182ecebfd22f3729beef0636d151d94764a1c28330d185e4e5",
-                "sha256:ac162b4ebe51b7a2b7f5e462c4402802633eb81e77c94f8a7c1ed8a556e72c75",
-                "sha256:b6187378726c84365bf297b5dcdae8789b6a5823b200bea23797777e5a63be09",
-                "sha256:bcd5723d905ed4a825f17410a53535f880b6d7548ae3d89078db7b1ceefcd853",
-                "sha256:c48a4f9c5fb385269bb7fbaf9c1326a94863b65ec7f5c96b2ea56b252f01ad08",
-                "sha256:cd40199d6f1c29c85b170d25589be9a97edff8ee7e62be180a2a137823896030",
-                "sha256:d1bc331a7d069485ac1d8c25a0ea1f6aab6cb2a87146fb652222481c1bddc9ff",
-                "sha256:d7e0cdc249aa0f94aa2e531b03999ddaf03a10b4fa090a894712d4c8066abd89",
-                "sha256:e9ee8fcd8e067fcc5d7276d46e07e863102b70a52545ef4254df1ff0893ce75f",
-                "sha256:eb313c23d983b7810504f42104e8dcd1c7ccdda8fbaab82aab92ab79fea19345",
-                "sha256:f9cfd478654b509941b85ed70f870f5e3c74678f566bec12fd26545e5340ba47",
-                "sha256:fae1fa144034d021a52cb9ea200eb8dedf91869c6df8202ad5d149b41ed91cc8"
-            ],
-            "version": "==5.0a5"
-        },
-        "coveralls": {
-            "hashes": [
-                "sha256:a8de28a5f04e418c7142b8ce6588c3a64245b433c458a5871cb043383667e4f2",
-                "sha256:c5e50b73b980d89308816b597e3e7bdeb0adedf831585d5c4ac967d576f8925d"
-            ],
-            "index": "pypi",
-            "version": "==1.8.0"
-        },
-        "decorator": {
-            "hashes": [
-                "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de",
-                "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"
-            ],
-            "version": "==4.4.0"
-        },
-        "docopt": {
-            "hashes": [
-                "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
-            ],
-            "version": "==0.6.2"
-        },
-        "docutils": {
-            "hashes": [
-                "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
-                "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
-                "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
-            ],
-            "version": "==0.14"
-        },
-        "entrypoints": {
-            "hashes": [
-                "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
-                "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
-            ],
-            "version": "==0.3"
-        },
-        "flake8": {
-            "hashes": [
-                "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661",
-                "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8"
-            ],
-            "version": "==3.7.7"
-        },
-        "flake8-polyfill": {
-            "hashes": [
-                "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9",
-                "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"
-            ],
-            "version": "==1.0.2"
-        },
-        "future": {
-            "hashes": [
-                "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"
-            ],
-            "version": "==0.17.1"
-        },
-        "gitdb2": {
-            "hashes": [
-                "sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2",
-                "sha256:e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a"
-            ],
-            "version": "==2.0.5"
-        },
-        "gitpython": {
-            "hashes": [
-                "sha256:563221e5a44369c6b79172f455584c9ebbb122a13368cc82cb4b5addff788f82",
-                "sha256:8237dc5bfd6f1366abeee5624111b9d6879393d84745a507de0fda86043b65a8"
-            ],
-            "version": "==2.1.11"
-        },
-        "gprof2dot": {
-            "hashes": [
-                "sha256:cebc7aa2782fd813ead415ea1fae3409524343485eadc7fb60ef5bd1e810309e"
-            ],
-            "version": "==2017.9.19"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
-                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
-            ],
-            "version": "==2.8"
-        },
-        "imagesize": {
-            "hashes": [
-                "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
-                "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
-            ],
-            "version": "==1.1.0"
-        },
-        "importlib-metadata": {
-            "hashes": [
-                "sha256:a9f185022cfa69e9ca5f7eabfd5a58b689894cb78a11e3c8c89398a8ccbb8e7f",
-                "sha256:df1403cd3aebeb2b1dcd3515ca062eecb5bd3ea7611f18cba81130c68707e879"
-            ],
-            "version": "==0.17"
-        },
-        "ipython-genutils": {
-            "hashes": [
-                "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
-                "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
-            ],
-            "version": "==0.2.0"
-        },
-        "isort": {
-            "hashes": [
-                "sha256:c40744b6bc5162bbb39c1257fe298b7a393861d50978b565f3ccd9cb9de0182a",
-                "sha256:f57abacd059dc3bd666258d1efb0377510a89777fda3e3274e3c01f7c03ae22d"
-            ],
-            "version": "==4.3.20"
-        },
-        "jinja2": {
-            "hashes": [
-                "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
-                "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
-            ],
-            "version": "==2.10.1"
-        },
-        "jsonschema": {
-            "hashes": [
-                "sha256:0c0a81564f181de3212efa2d17de1910f8732fa1b71c42266d983cd74304e20d",
-                "sha256:a5f6559964a3851f59040d3b961de5e68e70971afb88ba519d27e6a039efff1a"
-            ],
-            "version": "==3.0.1"
-        },
-        "jupyter-core": {
-            "hashes": [
-                "sha256:927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970",
-                "sha256:ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7"
-            ],
-            "version": "==4.4.0"
-        },
-        "lazy-object-proxy": {
-            "hashes": [
-                "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661",
-                "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f",
-                "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13",
-                "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821",
-                "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71",
-                "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e",
-                "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea",
-                "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229",
-                "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4",
-                "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e",
-                "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20",
-                "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16",
-                "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b",
-                "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7",
-                "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c",
-                "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a",
-                "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e",
-                "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1"
-            ],
-            "version": "==1.4.1"
-        },
-        "mando": {
-            "hashes": [
-                "sha256:4ce09faec7e5192ffc3c57830e26acba0fd6cd11e1ee81af0d4df0657463bd1c",
-                "sha256:79feb19dc0f097daa64a1243db578e7674909b75f88ac2220f1c065c10a0d960"
-            ],
-            "version": "==0.6.4"
-        },
-        "markupsafe": {
-            "hashes": [
-                "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
-                "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
-                "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
-                "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
-                "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
-                "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
-                "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
-                "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
-                "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
-                "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
-                "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
-                "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
-                "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
-                "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
-                "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
-                "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
-                "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
-                "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
-                "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
-                "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
-                "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
-                "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
-                "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
-                "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
-                "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
-                "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
-                "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
-                "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
-            ],
-            "version": "==1.1.1"
-        },
-        "mccabe": {
-            "hashes": [
-                "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
-                "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
-            ],
-            "version": "==0.6.1"
-        },
-        "more-itertools": {
-            "hashes": [
-                "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
-                "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
-            ],
-            "markers": "python_version > '2.7'",
-            "version": "==7.0.0"
-        },
-        "mypy": {
-            "hashes": [
-                "sha256:2afe51527b1f6cdc4a5f34fc90473109b22bf7f21086ba3e9451857cf11489e6",
-                "sha256:56a16df3e0abb145d8accd5dbb70eba6c4bd26e2f89042b491faa78c9635d1e2",
-                "sha256:5764f10d27b2e93c84f70af5778941b8f4aa1379b2430f85c827e0f5464e8714",
-                "sha256:5bbc86374f04a3aa817622f98e40375ccb28c4836f36b66706cf3c6ccce86eda",
-                "sha256:6a9343089f6377e71e20ca734cd8e7ac25d36478a9df580efabfe9059819bf82",
-                "sha256:6c9851bc4a23dc1d854d3f5dfd5f20a016f8da86bcdbb42687879bb5f86434b0",
-                "sha256:b8e85956af3fcf043d6f87c91cbe8705073fc67029ba6e22d3468bfee42c4823",
-                "sha256:b9a0af8fae490306bc112229000aa0c2ccc837b49d29a5c42e088c132a2334dd",
-                "sha256:bbf643528e2a55df2c1587008d6e3bda5c0445f1240dfa85129af22ae16d7a9a",
-                "sha256:c46ab3438bd21511db0f2c612d89d8344154c0c9494afc7fbc932de514cf8d15",
-                "sha256:f7a83d6bd805855ef83ec605eb01ab4fa42bcef254b13631e451cbb44914a9b0"
-            ],
-            "index": "pypi",
-            "version": "==0.701"
-        },
-        "mypy-extensions": {
-            "hashes": [
-                "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812",
-                "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"
-            ],
-            "version": "==0.4.1"
-        },
-        "nbformat": {
-            "hashes": [
-                "sha256:b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b",
-                "sha256:f7494ef0df60766b7cabe0a3651556345a963b74dbc16bc7c18479041170d402"
-            ],
-            "version": "==4.4.0"
-        },
-        "packaging": {
-            "hashes": [
-                "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
-                "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
-            ],
-            "version": "==19.0"
-        },
-        "pbr": {
-            "hashes": [
-                "sha256:0ce920b865091450bbcd452b35cf6d6eb8a6d9ce13ad2210d6e77557f85cf32b",
-                "sha256:93d2dc6ee0c9af4dbc70bc1251d0e545a9910ca8863774761f92716dece400b6"
-            ],
-            "version": "==5.2.1"
-        },
-        "pipenv": {
-            "hashes": [
-                "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330",
-                "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e",
-                "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846"
-            ],
-            "version": "==2018.11.26"
-        },
-        "pipenv-to-requirements": {
-            "hashes": [
-                "sha256:04dc5b1fedfea18858efe3f28d62ed70ac0cd04c34bc32a8c3531384d11be460",
-                "sha256:44bfefa3f6ded62e3bdcc5a6bb4de7a821b2658f821c1126f6abc3bffecb2c1e"
-            ],
-            "index": "pypi",
-            "version": "==0.7.1"
-        },
-        "plotly": {
-            "hashes": [
-                "sha256:5dcb33f5c9a00e1d68ac567340b58e90c07ba93767ad3e360dd6aa11cf568a98",
-                "sha256:ab51c41f3212c5b50238abc1be070cdf79dea60a127216e7c2f1829d14113b49"
-            ],
-            "version": "==3.10.0"
-        },
-        "pluggy": {
-            "hashes": [
-                "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc",
-                "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"
-            ],
-            "version": "==0.12.0"
-        },
-        "progress": {
-            "hashes": [
-                "sha256:69ecedd1d1bbe71bf6313d88d1e6c4d2957b7f1d4f71312c211257f7dae64372"
-            ],
-            "version": "==1.5"
-        },
-        "py": {
-            "hashes": [
-                "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
-                "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
-            ],
-            "version": "==1.8.0"
-        },
-        "pycodestyle": {
-            "hashes": [
-                "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
-                "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
-            ],
-            "version": "==2.5.0"
-        },
-        "pydocstyle": {
-            "hashes": [
-                "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8",
-                "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4",
-                "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039"
-            ],
-            "index": "pypi",
-            "version": "==3.0.0"
-        },
-        "pyflakes": {
-            "hashes": [
-                "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
-                "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
-            ],
-            "version": "==2.1.1"
-        },
-        "pygments": {
-            "hashes": [
-                "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
-                "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
-            ],
-            "version": "==2.4.2"
-        },
-        "pylint": {
-            "hashes": [
-                "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09",
-                "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1"
-            ],
-            "index": "pypi",
-            "version": "==2.3.1"
-        },
-        "pyparsing": {
-            "hashes": [
-                "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
-                "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
-            ],
-            "version": "==2.4.0"
-        },
-        "pyrsistent": {
-            "hashes": [
-                "sha256:16692ee739d42cf5e39cef8d27649a8c1fdb7aa99887098f1460057c5eb75c3a"
-            ],
-            "version": "==0.15.2"
-        },
-        "pytest": {
-            "hashes": [
-                "sha256:6032845e68a17a96e8da3088037f899b56357769a724122056265ca2ea1890ee",
-                "sha256:bea27a646a3d74cbbcf8d3d4a06b2dfc336baf3dc2cc85cf70ad0157e73e8322"
-            ],
-            "index": "pypi",
-            "version": "==4.6.2"
-        },
-        "pytest-cov": {
-            "hashes": [
-                "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6",
-                "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"
-            ],
-            "index": "pypi",
-            "version": "==2.7.1"
-        },
-        "pytest-profiling": {
-            "hashes": [
-                "sha256:93938f147662225d2b8bd5af89587b979652426a8a6ffd7e73ec4a23e24b7f29",
-                "sha256:999cc9ac94f2e528e3f5d43465da277429984a1c237ae9818f8cfd0b06acb019"
-            ],
-            "index": "pypi",
-            "version": "==1.7.0"
-        },
-        "pytz": {
-            "hashes": [
-                "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
-                "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
-            ],
-            "version": "==2019.1"
-        },
-        "radon": {
-            "hashes": [
-                "sha256:0cde1953547a164d24420ed6ccdfa18b61f1457b96a2b99ff0de76b22d504a0f",
-                "sha256:ee20308ce8bae7a89b067425b63b141a0077632ab318d5288da649c830882b3d"
-            ],
-            "version": "==3.0.3"
-        },
-        "requests": {
-            "hashes": [
-                "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
-                "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
-            ],
-            "index": "pypi",
-            "version": "==2.22.0"
-        },
-        "retrying": {
-            "hashes": [
-                "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b"
-            ],
-            "version": "==1.3.3"
-        },
-        "rope": {
-            "hashes": [
-                "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969",
-                "sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf",
-                "sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"
-            ],
-            "index": "pypi",
-            "version": "==0.14.0"
-        },
-        "six": {
-            "hashes": [
-                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
-                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
-            ],
-            "version": "==1.12.0"
-        },
-        "smmap2": {
-            "hashes": [
-                "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde",
-                "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"
-            ],
-            "version": "==2.0.5"
-        },
-        "snowballstemmer": {
-            "hashes": [
-                "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
-                "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
-            ],
-            "version": "==1.2.1"
-        },
-        "sphinx": {
-            "hashes": [
-                "sha256:2c5becc0fd6706dc0aeb4703f9f1f8a1d1eecacf02e9ac5943cbae48b11e5e42",
-                "sha256:7a359a91fb04054ec77d68ff97cb8728f8cc322e25f22dc94299d67e0e6a7123"
-            ],
-            "index": "pypi",
-            "version": "==2.1.0"
-        },
-        "sphinx-autodoc-typehints": {
-            "hashes": [
-                "sha256:19fe0b426b7c008181f67f816060da7f046bd8a42723f67a685d26d875bcefd7",
-                "sha256:f9c06acfec80766fe8f542a6d6a042e751fcf6ce2e2711a7dc00d8b6daf8aa36"
-            ],
-            "index": "pypi",
-            "version": "==1.6.0"
-        },
-        "sphinxcontrib-applehelp": {
-            "hashes": [
-                "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
-                "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
-            ],
-            "version": "==1.0.1"
-        },
-        "sphinxcontrib-devhelp": {
-            "hashes": [
-                "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
-                "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
-            ],
-            "version": "==1.0.1"
-        },
-        "sphinxcontrib-htmlhelp": {
-            "hashes": [
-                "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
-                "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
-            ],
-            "version": "==1.0.2"
-        },
-        "sphinxcontrib-jsmath": {
-            "hashes": [
-                "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
-                "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
-            ],
-            "version": "==1.0.1"
-        },
-        "sphinxcontrib-qthelp": {
-            "hashes": [
-                "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
-                "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
-            ],
-            "version": "==1.0.2"
-        },
-        "sphinxcontrib-serializinghtml": {
-            "hashes": [
-                "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
-                "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
-            ],
-            "version": "==1.1.3"
-        },
-        "tabulate": {
-            "hashes": [
-                "sha256:8af07a39377cee1103a5c8b3330a421c2d99b9141e9cc5ddd2e3263fea416943"
-            ],
-            "version": "==0.8.3"
-        },
-        "toml": {
-            "hashes": [
-                "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
-                "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
-            ],
-            "version": "==0.10.0"
-        },
-        "traitlets": {
-            "hashes": [
-                "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835",
-                "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"
-            ],
-            "version": "==4.3.2"
-        },
-        "typed-ast": {
-            "hashes": [
-                "sha256:132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b",
-                "sha256:18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d",
-                "sha256:2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a",
-                "sha256:3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462",
-                "sha256:4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee",
-                "sha256:4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a",
-                "sha256:5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4",
-                "sha256:6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649",
-                "sha256:7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a",
-                "sha256:8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f",
-                "sha256:8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7",
-                "sha256:912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760",
-                "sha256:b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18",
-                "sha256:c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616",
-                "sha256:c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd",
-                "sha256:ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21",
-                "sha256:eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93",
-                "sha256:f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb",
-                "sha256:f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7"
-            ],
-            "markers": "implementation_name == 'cpython'",
-            "version": "==1.3.5"
-        },
-        "urllib3": {
-            "hashes": [
-                "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
-                "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
-            ],
-            "version": "==1.25.3"
-        },
-        "virtualenv": {
-            "hashes": [
-                "sha256:99acaf1e35c7ccf9763db9ba2accbca2f4254d61d1912c5ee364f9cc4a8942a0",
-                "sha256:fe51cdbf04e5d8152af06c075404745a7419de27495a83f0d72518ad50be3ce8"
-            ],
-            "version": "==16.6.0"
-        },
-        "virtualenv-clone": {
-            "hashes": [
-                "sha256:532f789a5c88adf339506e3ca03326f20ee82fd08ee5586b44dc859b5b4468c5",
-                "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557"
-            ],
-            "version": "==0.5.3"
-        },
-        "wcwidth": {
-            "hashes": [
-                "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
-                "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
-            ],
-            "version": "==0.1.7"
-        },
-        "wily": {
-            "hashes": [
-                "sha256:3c9a9f931d051ca7874df035c0518aa67569ac68da06c6cca0823626277130b9",
-                "sha256:e13810f60cb436b7dc0aa6a70a584db297bbecf8553ba2981b822e4160d06ba4"
-            ],
-            "index": "pypi",
-            "version": "==1.12.2"
-        },
-        "wrapt": {
-            "hashes": [
-                "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
-            ],
-            "version": "==1.11.1"
-        },
-        "zipp": {
-            "hashes": [
-                "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d",
-                "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3"
-            ],
-            "version": "==0.5.1"
-        }
-    }
-}
diff --git a/authorityspoke/__init__.py b/authorityspoke/__init__.py
index 4179fb26..7eda9040 100644
--- a/authorityspoke/__init__.py
+++ b/authorityspoke/__init__.py
@@ -4,6 +4,6 @@
 from .factors import Factor, Entity
 from .jurisdictions import Jurisdiction, Regime
 from .opinions import Opinion
-from .rules import Rule, ProceduralRule
+from .rules import Rule
 
 __version__ = "0.1.0"
diff --git a/authorityspoke/enactments.py b/authorityspoke/enactments.py
index e2eb29cb..5a61304f 100644
--- a/authorityspoke/enactments.py
+++ b/authorityspoke/enactments.py
@@ -6,16 +6,16 @@
 import functools
 import pathlib
 import re
-from typing import Dict, List, Optional, Union
+from typing import Dict, List, Optional, Tuple, Union
 from dataclasses import dataclass
 
 from bs4 import BeautifulSoup
 
-from utils import roman
+from utils.cache import lazyprop
+from utils.roman import from_roman
 
 from authorityspoke.context import log_mentioned_context, get_directory_path
 from authorityspoke.selectors import TextQuoteSelector
-from utils.cache import lazyprop
 
 
 class Code:
@@ -205,7 +205,7 @@ def provision_effective_date(
             if "amendment" not in cite.lower():
                 return datetime.date(1788, 9, 13)
             roman_numeral = cite.split("-")[1]
-            amendment_number = roman.from_roman(roman_numeral)
+            amendment_number = from_roman(roman_numeral)
             if amendment_number < 11:
                 return datetime.date(1791, 12, 15)
             section = self.xml.find(id=cite)
@@ -227,23 +227,17 @@ def provision_effective_date(
 
         raise NotImplementedError
 
-    def select_text(self, selector: TextQuoteSelector) -> Optional[str]:
+    @functools.lru_cache()
+    def section_text(self, path: str) -> str:
         """
-        Get text from the ``Code`` using a :class:`.TextQuoteSelector`.
+        Get the text of the section identified by a path.
 
-        :param selector:
-            a selector referencing a text passage in the ``Code``.
-
-        .. note::
-            When handling Code of Federal Regulation (CFR) :class:`.Enactment`\s,
-            this can only select from the whole document or from Sections,
-            not Subsections or any other level. Still hoping to be able
-            to switch to a `United States Legislative Markup (USLM)
-            <https://github.com/usgpo/uslm>`_-like XML format for CFR.
+        :param path:
+            a path string, in the format used for :class:`.TextQuoteSelector`
+            objects, to the section with the text to be returned.
 
         :returns:
-            the text referenced by the selector, or ``None`` if the text
-            can't be found.
+            the text of a section of the :class:`Code`.
         """
 
         def cal_href(docpath, href):
@@ -258,13 +252,13 @@ def cal_href(docpath, href):
             ).search(href)
 
         def usc_statute_text():
-            section = self.xml.find(identifier=selector.path)
+            section = self.xml.find(identifier=path)
             return section.find_all(["chapeau", "paragraph", "content"])
 
-        if selector.path is not None:
-            docpath = selector.path.replace(self.uri, "")
+        if path is not None:
+            docpath = path.replace(self.uri, "")
 
-        if selector.path is None:  # selecting the whole Code
+        if path is None:  # selecting the whole Code
             passages = self.xml.find_all(name="text")
         elif self.jurisdiction == "us":
             if self.level == "regulation":
@@ -283,19 +277,55 @@ def usc_statute_text():
                 style="margin:0;display:inline;"
             )
 
-        text = " ".join(" ".join(passage.text.split()) for passage in passages)
+        return " ".join(" ".join(passage.text.split()) for passage in passages)
+
+    def select_text_from_interval(
+        self, interval: Tuple[int, int], path: Optional[str] = None
+    ) -> Optional[str]:
+        """
+        Select text as interval of section identified by path.
+
+        :param interval:
+            the indices of the slice of the section text to be selected.
+
+        :para path:
+            a path to the section that text should be selected from
+
+        :returns:
+            a slice of text from the section identified by ``path``
+        """
+
+        if not path:
+            path = self.uri
+        section_text = self.section_text(path)
+        if len(section_text) < max(interval):
+            raise ValueError(
+                f"Interval {interval} extends beyond the end of Enactment {path}."
+            )
+        return section_text[min(interval) : max(interval)]
+
+    def select_text(self, selector: TextQuoteSelector) -> Optional[str]:
+        """
+        Get text from the ``Code`` using a :class:`.TextQuoteSelector`.
+
+        :param selector:
+            a selector referencing a text passage in the ``Code``.
+
+        .. note::
+            When handling Code of Federal Regulation (CFR) :class:`.Enactment`\s,
+            this can only select from the whole document or from Sections,
+            not Subsections or any other level. Still hoping to be able
+            to switch to a `United States Legislative Markup (USLM)
+            <https://github.com/usgpo/uslm>`_-like XML format for CFR.
+
+        :returns:
+            the text referenced by the selector, or ``None`` if the text
+            can't be found.
+        """
+        text = self.section_text(path=selector.path)
         if not selector.exact:
             return text
-        prefix = selector.prefix or ""
-        suffix = selector.suffix or ""
-        passage_regex = (
-            re.escape(prefix)
-            + r"\s*"
-            + re.escape(selector.exact)
-            + r"\s*"
-            + re.escape(suffix)
-        )
-        if re.search(passage_regex, text, re.IGNORECASE):
+        if re.search(selector.passage_regex, text, re.IGNORECASE):
             return selector.exact
         raise ValueError(
             f"Passage {selector.exact} from TextQuoteSelector "
@@ -315,7 +345,7 @@ class Enactment:
     A passage of legislative text.
 
     May be used as support for a
-    :class:`.ProceduralRule`. To retrieve the text, there needs
+    :class:`.Rule`. To retrieve the text, there needs
     to be an available method for identifying the correct XML
     element based on the section and subsection names, and each
     XML format used for any :class:`Code` will require a different method.
@@ -339,7 +369,7 @@ class Enactment:
 
     selector: TextQuoteSelector
     code: Optional[Code] = None
-    regime: Optional["Regime"] = None
+    regime: Optional[Regime] = None
     name: Optional[str] = None
 
     def __post_init__(self):
@@ -354,6 +384,81 @@ def __post_init__(self):
             self.selector.set_exact_from_source(self.code)
         object.__delattr__(self, "regime")
 
+    def __add__(self, other):
+        if not isinstance(other, self.__class__):
+            raise TypeError(
+                f"Objects of type {self.__class__} may only be added "
+                + "to other objects of the same class."
+            )
+        if self >= other:
+            return self
+        if other >= self:
+            return other
+
+        combined = self.combine_text(other)
+        if combined:
+            return combined
+        combined = other.combine_text(self)
+        if combined:
+            return combined
+
+        # If the Enactments can't be combined, return None
+        return None
+
+    def combine_text(self, other: Enactment) -> Optional[Enactment]:
+        """
+        Create new :class:`Enactment` with combined text of the source :class:`Enactment`\s.
+
+        :param other:
+            another :class:`Enactment` with text to combine with the text from ``self``.
+
+        :returns:
+            new :class:`Enactment` with combined text of the source :class:`Enactment`\s, or
+            ``None`` if the :class:`Enactment`\s can't be combined.
+        """
+        if not other.selector.path.startswith(self.selector.path):
+            return None
+        self_interval = self.text_interval()
+        other_interval = other.text_interval(path=self.selector.path)
+        if not self_interval or not other_interval:
+            return None
+        both_intervals = sorted([self_interval, other_interval])
+        if both_intervals[1][0] >= both_intervals[0][1] + 2:
+            return None
+        new_interval = (
+            both_intervals[0][0],
+            max(both_intervals[0][1], both_intervals[1][1]),
+        )
+        # BUG: can't create prefix and suffix to distinguish identical passages.
+        return Enactment(
+            selector=TextQuoteSelector(
+                path=self.selector.path,
+                exact=self.code.select_text_from_interval(
+                    interval=new_interval, path=self.selector.path
+                ),
+            ),
+            code=self.code,
+        )
+
+    def text_interval(self, path=None) -> Optional[Tuple[int, int]]:
+        """
+        Find integer indices for the quoted text.
+
+        :returns:
+            A :class:`tuple` containing the lower and upper bounds of the
+            text passage quoted in ``self.selector.exact`` within the
+            XML section referenced in ``self.selector.path``.
+        """
+        if not path:
+            path = self.selector.path
+        regex = self.selector.passage_regex
+        match = re.search(regex, self.code.section_text(path), re.IGNORECASE)
+        if match:
+            # Getting indices from match group 1 (in the parentheses),
+            # not match 0 which includes prefix and suffix
+            return (match.start(1), match.end(1))
+        return None
+
     @property
     def effective_date(self):
         """
diff --git a/authorityspoke/factors.py b/authorityspoke/factors.py
index 355258c7..0e209d46 100644
--- a/authorityspoke/factors.py
+++ b/authorityspoke/factors.py
@@ -186,6 +186,15 @@ def recursive_factors(self) -> Dict[Factor, None]:
                 answers.update(context.recursive_factors)
         return answers
 
+    def __add__(self, other) -> Optional[Factor]:
+        if not isinstance(other, Factor):
+            raise TypeError
+        if self >= other:
+            return self
+        if other >= self:
+            return other.new_context(self.generic_factors)
+        return None
+
     def consistent_with(self, other: Factor, comparison: Callable) -> bool:
         """
         Find whether ``self`` and ``other`` can fit the relationship ``comparison``.
@@ -199,6 +208,14 @@ def consistent_with(self, other: Factor, comparison: Callable) -> bool:
         context_registers = iter(self._context_registers(other, comparison))
         return any(register is not None for register in context_registers)
 
+    def _contradicts_if_present(self, other: Factor) -> bool:
+        """
+        Test if ``self`` would contradict ``other`` if neither was ``absent``.
+
+        The default is ``False`` where no class-specific method is available.
+        """
+        return False
+
     def _context_registers(
         self, other: Factor, comparison: Callable
     ) -> Iterator[Dict[Factor, Factor]]:
@@ -343,14 +360,15 @@ def __ge__(self, other: Factor) -> bool:
                 f"{self.__class__} objects may only be compared for "
                 + "implication with other Factor objects or None."
             )
+        if not self.__dict__.get("absent"):
+            if not other.__dict__.get("absent"):
+                return bool(self._implies_if_present(other))
+            return bool(self._contradicts_if_present(other))
 
-        if self.__dict__.get("absent") and other.__dict__.get("absent"):
+        # if self.__dict__.get("absent")
+        if other.__dict__.get("absent"):
             return bool(other._implies_if_present(self))
-
-        if not self.__dict__.get("absent") and not other.__dict__.get("absent"):
-            return bool(self._implies_if_present(other))
-
-        return False
+        return bool(other._contradicts_if_present(self))
 
     def __gt__(self, other: Optional[Factor]) -> bool:
         """Test whether ``self`` implies ``other`` and ``self`` != ``other``."""
@@ -551,11 +569,12 @@ def update_context_register(
 
     @staticmethod
     def _wrap_with_tuple(item):
+        if item is None:
+            return ()
         if isinstance(item, Iterable):
             return tuple(item)
         return (item,)
 
-
 @dataclass(frozen=True)
 class Fact(Factor):
     """
@@ -823,15 +842,13 @@ def _implies_if_concrete(self, other: Factor) -> bool:
 
     def _contradicts_if_present(self, other: Fact) -> bool:
         """
-        Test if ``self`` contradicts ``other``, assuming they are not ``absent``.
+        Test if ``self`` contradicts :class:`Fact` ``other`` if neither is ``absent``.
 
         :returns:
             whether ``self`` and ``other`` can't both be true at
             the same time under the given assumption.
         """
-        if (self.predicate.contradicts(other.predicate) and not other.absent) or (
-            self.predicate >= other.predicate and other.absent
-        ):
+        if self.predicate.contradicts(other.predicate):
             return self.consistent_with(other, operator.ge)
         return False
 
@@ -848,8 +865,12 @@ def _contradicts_if_factor(self, other: Factor) -> bool:
             return False
 
         if self.absent:
-            return other._contradicts_if_present(self)
-        return self._contradicts_if_present(other)
+            if other.absent:
+                return other._contradicts_if_present(self)
+            return other._implies_if_present(self)
+        if not other.absent:
+            return self._contradicts_if_present(other)
+        return self._implies_if_present(other)
 
     @classmethod
     def _build_from_dict(
diff --git a/authorityspoke/jurisdictions.py b/authorityspoke/jurisdictions.py
index 463c8843..156e64bb 100644
--- a/authorityspoke/jurisdictions.py
+++ b/authorityspoke/jurisdictions.py
@@ -9,7 +9,7 @@
 from __future__ import annotations
 
 from pathlib import PurePosixPath
-from typing import Dict, List, Optional, Union
+from typing import ClassVar, Dict, List, Optional, Union
 
 from dataclasses import dataclass, field
 
@@ -73,6 +73,7 @@ class Regime:
     """
 
     jurisdictions: Dict[str, Jurisdiction] = field(default_factory=dict)
+    uri: ClassVar = "/"
 
     def get_code(self, selector: Union[TextQuoteSelector, str]):
         """
diff --git a/authorityspoke/opinions.py b/authorityspoke/opinions.py
index 6623a782..0f7e7a14 100644
--- a/authorityspoke/opinions.py
+++ b/authorityspoke/opinions.py
@@ -20,7 +20,7 @@
 
 from authorityspoke.context import get_directory_path
 from authorityspoke.factors import Factor
-from authorityspoke.rules import Rule, ProceduralRule
+from authorityspoke.rules import Rule
 
 
 @dataclass
@@ -457,7 +457,7 @@ def posits_one_holding(
             if not isinstance(holding, Rule):
                 raise TypeError('"holding" must be an object of type Rule.')
 
-            if context is not None and isinstance(holding, ProceduralRule):
+            if context is not None and isinstance(holding, Rule):
                 if isinstance(context, dict):
                     for factor in context:
                         if isinstance(context[factor], str):
diff --git a/authorityspoke/predicates.py b/authorityspoke/predicates.py
index 23713389..491c2088 100644
--- a/authorityspoke/predicates.py
+++ b/authorityspoke/predicates.py
@@ -77,13 +77,23 @@ class Predicate:
     }
 
     def __post_init__(self):
+        """
+        Clean up and test validity of attributes.
+
+        If the :attr:`content` sentence is phrased to have a plural
+        context factor, normalizes it by changing "were" to "was".
 
+        Conjugating verbs using regex feels like a very brittle solution.
+        An NLP library may be used here in future versions.
+        """
         normalize_comparison = {"==": "=", "!=": "<>"}
         if self.comparison in normalize_comparison:
             object.__setattr__(
                 self, "comparison", normalize_comparison[self.comparison]
             )
 
+        object.__setattr__(self, "content", self.content.replace("{} were", "{} was"))
+
         if self.comparison and self.comparison not in self.opposite_comparisons.keys():
             raise ValueError(
                 f'"comparison" string parameter must be one of {self.opposite_comparisons.keys()}.'
@@ -139,7 +149,14 @@ def content_with_entities(
                 f"Exactly {len(self)} entities needed to complete "
                 + f'"{self.content}", but {len(context)} were given.'
             )
-        return str(self).format(*(str(e) for e in context))
+        add_plurals = str(self)
+        for index, context_factor in enumerate(context):
+            if context_factor.__dict__.get("plural"):
+                add_plurals = Predicate.make_context_plural(
+                    sentence=add_plurals, index=index
+                )
+
+        return add_plurals.format(*(str(e) for e in context))
 
     def contradicts(self, other: Optional[Predicate]) -> bool:
         """
@@ -174,8 +191,7 @@ def contradicts(self, other: Optional[Predicate]) -> bool:
             return False
 
         if not (
-            self.content.lower() == other.content.lower()
-            and self.reciprocal == other.reciprocal
+            self.same_content_meaning(other) and self.reciprocal == other.reciprocal
         ):
             return False
 
@@ -200,7 +216,23 @@ def contradicts(self, other: Optional[Predicate]) -> bool:
                 if self.quantity == other.quantity:
                     return True
             return False
-        return self.content == other.content and self.truth != other.truth
+        return self.truth != other.truth
+
+    def same_content_meaning(self, other: Predicate) -> bool:
+        """
+        Test if :attr:`~Predicate.content` strings of ``self`` and ``other`` have same meaning.
+
+        This once was used to disregard differences between "was" and "were",
+        but that now happens in :meth:`Predicate.__post_init__`.
+
+        :param other:
+            another :class:`Predicate` being compared to ``self``
+
+        :returns:
+            whether ``self`` and ``other`` have :attr:`~Predicate.content` strings
+            similar enough to be considered to have the same meaning.
+        """
+        return self.content.lower() == other.content.lower()
 
     def means(self, other) -> bool:
         """
@@ -212,11 +244,10 @@ def means(self, other) -> bool:
         if not isinstance(other, self.__class__):
             return False
 
-        if (
-            self.content.lower() != other.content.lower()
-            or self.reciprocal != other.reciprocal
-            or self.quantity != other.quantity
-        ):
+        if not self.same_content_meaning(other):
+            return False
+
+        if self.reciprocal != other.reciprocal or self.quantity != other.quantity:
             return False
 
         return self.truth == other.truth and self.comparison == other.comparison
@@ -241,8 +272,7 @@ def __gt__(self, other: Optional[Predicate]) -> bool:
 
         # Assumes no predicate implies another based on meaning of their content text
         if not (
-            self.content.lower() == other.content.lower()
-            and self.reciprocal == other.reciprocal
+            self.same_content_meaning(other) and self.reciprocal == other.reciprocal
         ):
             return False
 
@@ -357,6 +387,41 @@ def __str__(self):
             content = self.content
         return f"{truth_prefix}{content}"
 
+    @staticmethod
+    def make_context_plural(sentence: str, index: int = 0) -> str:
+        """
+        Replace "was" with "were" after a context slot in a sentence.
+
+        :param sentence:
+            a sentence with pairs of curly braces representing slots for
+            context factors
+
+        :param index:
+            the index of the context factor that is plural, counting
+            from the start of the sentence
+
+        :returns:
+            a form of the sentence with one instance of "was" replaced
+            with "were"
+        """
+        pattern = re.compile(
+            r"""
+            ^       # from beginning of string
+            (       # start capture group \1
+            [^{]*?  # everything before the first {
+            (?:     # start noncapturing group \2
+            \{\}    # literal curly brackets next to each other
+            [^{]*?  # everything before the next literal curly bracket
+            ){%d}   # group \2 occurs "index" times (could be 0)
+            )       # end of \1, which will be in the re.sub replacement string
+            \{\}    # literal curly brackets
+            \ was   # literal " was"
+            """
+            % index,
+            re.VERBOSE,
+        )
+        return re.sub(pattern, r"\1{} were", sentence)
+
     @staticmethod
     def str_to_quantity(quantity: str) -> Union[float, int, ureg.Quantity]:
         """
diff --git a/authorityspoke/procedures.py b/authorityspoke/procedures.py
new file mode 100644
index 00000000..fe862de7
--- /dev/null
+++ b/authorityspoke/procedures.py
@@ -0,0 +1,477 @@
+"""
+Descriptions of changes in litigation, without specifying
+whether they are mandatory or universal, or specifying the
+:class:`.Enactment`\s that might require them.
+"""
+
+from __future__ import annotations
+
+import operator
+
+from typing import ClassVar, Dict, Iterable, List, Sequence, Tuple
+from typing import Optional, Union
+
+from dataclasses import dataclass
+
+from authorityspoke.factors import Factor, means, new_context_helper
+from authorityspoke.relations import Analogy, all_analogy_matches
+
+
+@dataclass(frozen=True)
+class Procedure(Factor):
+    """
+    A (potential) rule for courts to use in resolving litigation.
+
+    Described in terms of inputs and outputs, and also potentially
+    "despite" :class:`.Factor`\s, which occur when a :class:`Rule`
+    could be said to apply "even if" some "despite" factor is true.
+
+    Users generally should not need to interact with this class
+    directly, under the current design. Instead, they should interact
+    with the class :class:`.Rule`.
+
+    :param outputs:
+        an outcome that a court may accept based on the presence
+        of the ``inputs``
+
+    :param inputs:
+        supporting :class:`.Factor`\s in favor of the ``output``.
+        The ``input`` :class:`.Factor`\s are not treated as
+        potential undercutters.
+
+    :param despite:
+        :class:`.Factor`\s that do not prevent the court from
+        imposing the ``output``. These could be considered
+        "failed undercutters" in defeasible logic. If a :class:`.Factor`
+        is relevant both as support for the output and as
+        a potential undercutter, include it in both ``inputs``
+        and ``despite``.
+
+    :param name:
+        An identifier that can be used to reference and
+        incorporate an existing procedure in a new
+        :class:`.Factor`, instead of constructing a new
+        copy of the :class:`.Procedure`.
+
+    :param absent:
+        Whether the absence, rather than presence, of this
+        :class:`.Procedure` is being referenced. The usefulness
+        of this is unclear, but I'm not prepared to eliminate it
+        before the :class:`.Argument` class has been implemented
+        and tested.
+
+    :param generic:
+        Whether the this :class:`Procedure` is being referenced
+        as a generic example, which could be replaced by any
+        other :class:`Procedure`.
+    """
+
+    outputs: Iterable[Factor] = ()
+    inputs: Iterable[Factor] = ()
+    despite: Iterable[Factor] = ()
+    name: Optional[str] = None
+    absent: bool = False
+    generic: bool = False
+    context_factor_names: ClassVar = ("outputs", "inputs", "despite")
+
+    def __post_init__(self):
+        outputs = self.__class__._wrap_with_tuple(self.outputs)
+        inputs = self.__class__._wrap_with_tuple(self.inputs)
+        despite = self.__class__._wrap_with_tuple(self.despite)
+        groups = {"outputs": outputs, "inputs": inputs, "despite": despite}
+        for group in groups:
+            for factor_obj in groups[group]:
+                if not isinstance(factor_obj, Factor):
+                    raise TypeError(
+                        "Input, Output, and Despite groups must contain "
+                        + "only subclasses of Factor, but "
+                        + f"{factor_obj} was type {type(factor_obj)}"
+                    )
+            object.__setattr__(self, group, groups[group])
+
+    def __len__(self):
+        """
+        Get number of generic :class:`.Factor`\s specified for ``self``.
+
+        :returns:
+            the number of generic :class:`.Factor`\s that need to be
+            specified for this :class:`Procedure`.
+        """
+
+        return len(self.generic_factors)
+
+    def __repr__(self):
+        return (
+            f"{self.__class__.__name__}(outputs=("
+            + f"{', '.join(repr(factor) for factor in self.outputs)}), "
+            + f"inputs=({', '.join(repr(factor) for factor in self.inputs)}), "
+            + f"despite=({', '.join(repr(factor) for factor in self.despite)}))"
+        )
+
+    def __str__(self):
+        text = "Procedure:"
+        if self.inputs:
+            text += "\nSupporting inputs:"
+            for f in self.inputs:
+                text += "\n" + str(f)
+        if self.despite:
+            text += "\nDespite:"
+            for f in self.despite:
+                text += "\n" + str(f)
+        if self.outputs:
+            text += "\nOutputs:"
+            for f in self.outputs:
+                text += "\n" + str(f)
+        return text
+
+    @property
+    def factors_all(self) -> List[Factor]:
+        """
+        Get :class:`.Factor`\s in ``inputs``, ``outputs``, and ``despite``.
+
+        :returns:
+            a :class:`list` of all :class:`.Factor`\s.
+        """
+
+        inputs = self.inputs or ()
+        despite = self.despite or ()
+        return [*self.outputs, *inputs, *despite]
+
+    @property
+    def generic_factors(self) -> List[Optional[Factor]]:
+        """
+        :class:`.Factor`\s that can be replaced without changing ``self``\s meaning.
+
+        :returns:
+            ``self``'s generic :class:`.Factor`\s,
+            which must be matched to other generic :class:`.Factor`\s to
+            perform equality or implication tests between :class:`.Factor`\s
+            with :meth:`.Factor.means` or :meth:`.Factor.__ge__`.
+        """
+        if self.generic:
+            return [self]
+        return list(
+            {
+                generic: None
+                for factor in self.factors_all
+                for generic in factor.generic_factors
+            }
+        )
+
+    def consistent_factor_groups(
+        self,
+        self_factors: Tuple[Factor],
+        other_factors: Tuple[Factor],
+        matches: Dict[Factor, Factor],
+    ):
+        """
+        Find whether two sets of :class:`.Factor`\s can be consistent.
+
+        Works by first determining whether one :class:`.Factor`
+        potentially :meth:`~.Factor.contradicts` another,
+        and then determining whether it's possible to make
+        context assignments match between the contradictory
+        :class:`.Factor`\s.
+
+        .. Note::
+            Does ``Factor: None`` in matches always mean that
+            the :class:`.Factor` can avoid being matched in a
+            contradictory way?
+
+        :returns:
+            whether unassigned context factors can be assigned in such
+            a way that there's no contradiction between any factor in
+            ``self_factors`` and ``other_factors``, given that some
+            :class:`.Factor`\s have already been assigned as
+            described by ``matches``.
+        """
+        for self_factor in self_factors:
+            for other_factor in other_factors:
+                if self_factor.contradicts(other_factor):
+                    if all(
+                        all(
+                            matches.get(key) == context_register[key]
+                            or matches.get(context_register[key] == key)
+                            for key in self_factor.generic_factors
+                        )
+                        for context_register in self_factor._context_registers(
+                            other_factor, means
+                        )
+                    ):
+                        return False
+        return True
+
+    def contradiction_between_outputs(
+        self, other: Procedure, matches: Dict[Factor, Factor]
+    ) -> bool:
+        """
+        Test whether outputs of two :class:`Procedure`\s can contradict.
+
+        :param other:
+            another :class:`Factor`
+
+        :param matches:
+            keys representing :class:`.Factor`\s in ``self`` and
+            values representing :class:`.Factor`\s in ``other``. The
+            keys and values have been found in corresponding positions
+            in ``self`` and ``other``.
+
+        :returns:
+            whether any :class:`.Factor` assignment can be found that
+            makes a :class:`.Factor` in the output of ``other`` contradict
+            a :class:`.Factor` in the output of ``self``.
+        """
+        for other_factor in other.outputs:
+            for self_factor in self.outputs:
+                if other_factor.contradicts(self_factor):
+                    generic_pairs = zip(
+                        self_factor.generic_factors, other_factor.generic_factors
+                    )
+                    if all(matches.get(pair[0]) == pair[1] for pair in generic_pairs):
+                        return True
+        return False
+
+    def contradicts(self, other) -> bool:
+        """
+        Find if ``self`` applying in some cases implies ``other`` cannot apply in some.
+
+        Raises an error because, by analogy with :meth:`.Procedure.implies`\,
+        users might expect this method to return ``True`` only when
+        :class:`Rule` with ``universal=False`` and Procedure ``self``
+        would contradict another :class:`Rule` with ``universal=False``
+        and Procedure ``other``. But that would never happen.
+        """
+        raise NotImplementedError(
+            "Procedures do not contradict one another unless one of them ",
+            "applies in 'ALL' cases. Consider using ",
+            "'Procedure.contradicts_some_to_all' or 'Rule.contradicts'.",
+        )
+
+    def contradicts_some_to_all(self, other: Procedure) -> bool:
+        """
+        Find if ``self`` applying in some cases implies ``other`` cannot apply in all.
+
+        :returns:
+            whether the assertion that ``self`` applies in
+            **some** cases contradicts that ``other`` applies in **all**
+            cases, where at least one of the :class:`.Rule`\s is ``mandatory``.
+        """
+        if not isinstance(other, self.__class__):
+            return False
+
+        self_despite_or_input = (*self.despite, *self.inputs)
+
+        # For self to contradict other, every input of other
+        # must be implied by some input or despite factor of self.
+        relations = (Analogy(other.inputs, self_despite_or_input, operator.le),)
+        matchlist = all_analogy_matches(relations)
+
+        # For self to contradict other, some output of other
+        # must be contradicted by some output of self.
+
+        return any(self.contradiction_between_outputs(other, m) for m in matchlist)
+
+    def implies_all_to_all(self, other: Procedure) -> bool:
+        """
+        Find if ``self`` applying in all cases implies ``other`` applies in all.
+
+        ``self`` does not imply ``other`` if any output of ``other``
+        is not equal to or implied by some output of ``self``.
+
+        For ``self`` to imply ``other``, every input of ``self``
+        must be implied by some input of ``other``.
+
+        ``self`` does not imply ``other`` if any despite of ``other``
+        :meth:`~.Factor.contradicts` an input of ``self``.
+
+        :returns:
+            whether the assertion that ``self`` applies in **all** cases
+            implies that ``other`` applies in **all** cases.
+        """
+        if not isinstance(other, self.__class__):
+            return False
+
+        if self.means(other):
+            return True
+
+        relations = (
+            Analogy(other.outputs, self.outputs, operator.le),
+            Analogy(self.inputs, other.inputs, operator.le),
+        )
+        matchlist = all_analogy_matches(relations)
+
+        # For every factor in other, find the permutations of entity slots
+        # that are consistent with matchlist and that don't cause the factor
+        # to contradict any factor of self.
+
+        return any(
+            self.consistent_factor_groups(self.inputs, other.despite, matches)
+            for matches in matchlist
+        )
+
+    def implies(self, other: Procedure) -> bool:
+        """
+        Call :meth:`__ge__` as an alias.
+
+        :returns:
+            bool indicating whether ``self`` implies ``other``
+        """
+        return self >= other
+
+    def implies_all_to_some(self, other: Procedure) -> bool:
+        """
+        Find if ``self`` applying in all cases implies ``other`` applies in some.
+
+        For ``self`` to imply ``other``, every output of ``other``
+        must be equal to or implied by some output of ``self``.
+
+        For ``self`` to imply ``other``, every input of ``self`` must not be
+        contradicted by any input or despite of ``other``.
+
+        ``self`` does not imply ``other`` if any "despite" :class:`.Factor`\s
+        of ``other`` are not implied by inputs of ``self``.
+
+        :returns:
+            whether the assertion that ``self`` applies in **all** cases
+            implies that ``other`` applies in **some** cases (that is, if
+            the list of ``self``'s inputs is not considered an exhaustive list
+            of the circumstances needed to invoke the procedure).
+        """
+
+        if not isinstance(other, self.__class__):
+            return False
+
+        if self.implies_all_to_all(other):
+            return True
+
+        other_despite_or_input = (*other.despite, *other.inputs)
+        self_despite_or_input = (*self.despite, *self.inputs)
+
+        relations = (
+            Analogy(other.outputs, self.outputs, operator.le),
+            Analogy(other.despite, self_despite_or_input, operator.le),
+        )
+
+        matchlist = all_analogy_matches(relations)
+
+        return any(
+            self.consistent_factor_groups(self.inputs, other_despite_or_input, matches)
+            for matches in matchlist
+        )
+
+    def _implies_if_present(self, other: Factor) -> bool:
+        """
+        Find if ``self`` would imply ``other`` if they aren't absent.
+
+        When ``self`` and ``other`` are included in
+        :class:`Rule`\s that both apply in **some** cases:
+
+        ``self`` does not imply ``other`` if any input of ``other``
+        is not equal to or implied by some input of ``self``.
+
+        ``self`` does not imply ``other`` if any output of ``other``
+        is not equal to or implied by some output of self.
+
+        ``self`` does not imply ``other`` if any despite of ``other``
+        is not equal to or implied by some despite or input of ``self``.
+
+        :returns:
+            whether the assertion that ``self`` applies in some cases
+            implies that the :class:`.Procedure` ``other`` applies
+            in some cases.
+        """
+
+        despite_or_input = (*self.despite, *self.inputs)
+
+        relations = (
+            Analogy(other.outputs, self.outputs, operator.le),
+            Analogy(other.inputs, self.inputs, operator.le),
+            Analogy(other.despite, despite_or_input, operator.le),
+        )
+
+        return bool(all_analogy_matches(relations))
+
+    def means(self, other) -> bool:
+        """
+        Determine whether ``other`` has the same meaning as ``self``.
+
+        :returns:
+            whether the two :class:`.Procedure`\s have all the same
+            :class:`.Factor`\s with the same context factors in the
+            same roles.
+        """
+
+        if not isinstance(other, self.__class__):
+            return False
+
+            # Verifying that every factor in self is in other.
+            # Also verifying that every factor in other is in self.
+        groups = ("outputs", "inputs", "despite")
+        matchlist = [{}]
+        for group in groups:
+            updater = Analogy(
+                need_matches=self.__dict__[group],
+                available=other.__dict__[group],
+                comparison=means,
+            )
+            matchlist = updater.update_matchlist(matchlist)
+        if not bool(matchlist):
+            return False
+
+        # Now doing the same thing in reverse
+        matchlist = [{}]
+        for group in groups:
+            updater = Analogy(
+                need_matches=other.__dict__[group],
+                available=self.__dict__[group],
+                comparison=means,
+            )
+            matchlist = updater.update_matchlist(matchlist)
+        return bool(matchlist)
+
+    @new_context_helper
+    def new_context(self, changes: Dict[Factor, Factor]) -> Procedure:
+        """
+        Create new :class:`Procedure`, replacing keys of ``changes`` with values.
+
+        :param changes:
+            a :class:`dict` of :class:`.Factor`\s to replace
+            matched to the :class:`.Factor`\s that should replace them
+
+        :returns:
+            new :class:`.Procedure` object, replacing keys of
+            ``changes`` with their values.
+        """
+        new_dict = self.__dict__.copy()
+        for name in self.context_factor_names:
+            new_dict[name] = tuple(
+                [factor.new_context(changes) for factor in new_dict[name]]
+            )
+        return self.__class__(**new_dict)
+
+    def triggers_next_procedure(self, other: Procedure) -> List[Dict[Factor, Factor]]:
+        """
+        Test if :class:`.Factor`\s from firing ``self`` would trigger ``other``.
+
+        .. Note::
+            To be confident that ``self`` actually can trigger ``other``,
+            we would have to assume that ``other`` has ``universal: True``
+            since otherwise we don't know exactly what is required to
+            trigger ``other``.
+
+        :param other:
+            another :class:`Procedure` to test to see whether it can
+            be triggered by triggering ``self``
+
+        :returns:
+            whether the set of :class:`Factor`\s that exist after ``self``
+            is fired could trigger ``other``
+        """
+        self_despite_or_input = (*self.despite, *self.inputs)
+        self_output_or_input = (*self.outputs, *self.inputs)
+
+        relations = (
+            Analogy(other.inputs, self_output_or_input, operator.le),
+            Analogy(other.despite, self_despite_or_input, operator.le),
+        )
+        return all_analogy_matches(relations)
diff --git a/authorityspoke/relations.py b/authorityspoke/relations.py
index 2f4ceb87..354bfea1 100644
--- a/authorityspoke/relations.py
+++ b/authorityspoke/relations.py
@@ -153,3 +153,26 @@ def update_matchlist(
             for answer in self.unordered_comparison(matches=matches):
                 new_matchlist.append(answer)
         return new_matchlist
+
+
+def all_analogy_matches(
+    relations: Tuple[Analogy, ...]
+) -> List[Dict[Factor, Optional[Factor]]]:
+    """
+    Find all context registers consistent with multiple :class:`.Analogy` comparisons.
+
+    :param relations:
+        a series of :class:`.Analogy` comparisons in which
+        the ``need_matches`` :class:`.Factor`\s all refer to
+        one context (for instance, the same :class:`.Opinion`),
+        and the ``available`` :class:`.Factor`\s all refer to
+        another context.
+
+    :returns:
+        a list of all context registers consistent with all of the
+        :class:`.Analogy`\s.
+    """
+    matchlist = [{}]
+    for relation in relations:
+        matchlist = relation.update_matchlist(matchlist)
+    return matchlist
diff --git a/authorityspoke/rules.py b/authorityspoke/rules.py
index 06a6ad92..d00e143d 100644
--- a/authorityspoke/rules.py
+++ b/authorityspoke/rules.py
@@ -12,493 +12,182 @@
 import pathlib
 
 from typing import ClassVar, Dict, Iterable, List, Sequence, Tuple
-from typing import Optional, Union
+from typing import Iterator, Optional, Union
 
 from dataclasses import dataclass
 
 from authorityspoke.context import get_directory_path
 from authorityspoke.enactments import Enactment
 from authorityspoke.factors import Factor, means, new_context_helper
-from authorityspoke.relations import Analogy
+from authorityspoke.procedures import Procedure
 
 
 @dataclass(frozen=True)
-class Procedure(Factor):
-    """
-    A (potential) rule for courts to use in resolving litigation.
-
-    Described in terms of inputs and outputs, and also potentially
-    "despite" :class:`.Factor`\s, which occur when a :class:`Rule`
-    could be said to apply "even if" some "despite" factor is true.
-
-    Users generally should not need to interact with this class
-    directly, under the current design. Instead, they should interact
-    with the class :class:`.ProceduralRule`.
-
-    :param outputs:
-        an outcome that a court may accept based on the presence
-        of the ``inputs``
-
-    :param inputs:
-        supporting :class:`.Factor`\s in favor of the ``output``.
-        The ``input`` :class:`.Factor`\s are not treated as
-        potential undercutters.
-
-    :param despite:
-        :class:`.Factor`\s that do not prevent the court from
-        imposing the ``output``. These could be considered
-        "failed undercutters" in defeasible logic. If a :class:`.Factor`
-        is relevant both as support for the output and as
-        a potential undercutter, include it in both ``inputs``
-        and ``despite``.
-
-    :param name:
-        An identifier that can be used to reference and
-        incorporate an existing procedure in a new
-        :class:`.Factor`, instead of constructing a new
-        copy of the :class:`.Procedure`.
-
-    :param absent:
-        Whether the absence, rather than presence, of this
-        :class:`.Procedure` is being referenced. The usefulness
-        of this is unclear, but I'm not prepared to eliminate it
-        before the :class:`.Argument` class has been implemented
-        and tested.
+class Rule(Factor):
 
-    :param generic:
-        Whether the this :class:`Procedure` is being referenced
-        as a generic example, which could be replaced by any
-        other :class:`Procedure`.
     """
+    A statement of a legal doctrine about a :class:`.Procedure` for litigation.
 
-    outputs: Iterable[Factor] = ()
-    inputs: Iterable[Factor] = ()
-    despite: Iterable[Factor] = ()
-    name: Optional[str] = None
-    absent: bool = False
-    generic: bool = False
-    context_factor_names: ClassVar = ("outputs", "inputs", "despite")
-
-    def __post_init__(self):
-        outputs = self.__class__._wrap_with_tuple(self.outputs)
-        inputs = self.__class__._wrap_with_tuple(self.inputs)
-        despite = self.__class__._wrap_with_tuple(self.despite)
-        groups = {"outputs": outputs, "inputs": inputs, "despite": despite}
-        for group in groups:
-            for factor_obj in groups[group]:
-                if not isinstance(factor_obj, Factor):
-                    raise TypeError(
-                        "Input, Output, and Despite groups must contain "
-                        + "only subclasses of Factor, but "
-                        + f"{factor_obj} was type {type(factor_obj)}"
-                    )
-            object.__setattr__(self, group, groups[group])
-
-    def __len__(self):
-        """
-        Get number of generic :class:`.Factor`\s specified for ``self``.
-
-        :returns:
-            the number of generic :class:`.Factor`\s that need to be
-            specified for this :class:`Procedure`.
-        """
-
-        return len(self.generic_factors)
-
-    def __repr__(self):
-        return (
-            f"{self.__class__.__name__}(outputs=("
-            + f"{', '.join(repr(factor) for factor in self.outputs)}), "
-            + f"inputs=({', '.join(repr(factor) for factor in self.inputs)}), "
-            + f"despite=({', '.join(repr(factor) for factor in self.despite)}))"
-        )
-
-    def __str__(self):
-        text = "Procedure:"
-        if self.inputs:
-            text += "\nSupporting inputs:"
-            for f in self.inputs:
-                text += "\n" + str(f)
-        if self.despite:
-            text += "\nDespite:"
-            for f in self.despite:
-                text += "\n" + str(f)
-        if self.outputs:
-            text += "\nOutputs:"
-            for f in self.outputs:
-                text += "\n" + str(f)
-        return text
-
-    @property
-    def factors_all(self) -> List[Factor]:
-        """
-        Get :class:`.Factor`\s in ``inputs``, ``outputs``, and ``despite``.
-
-        :returns:
-            a :class:`list` of all :class:`.Factor`\s.
-        """
-
-        inputs = self.inputs or ()
-        despite = self.despite or ()
-        return [*self.outputs, *inputs, *despite]
-
-    @property
-    def generic_factors(self) -> List[Optional[Factor]]:
-        """
-        :class:`.Factor`\s that can be replaced without changing ``self``\s meaning.
-
-        :returns:
-            ``self``'s generic :class:`.Factor`\s,
-            which must be matched to other generic :class:`.Factor`\s to
-            perform equality or implication tests between :class:`.Factor`\s
-            with :meth:`.Factor.means` or :meth:`.Factor.__ge__`.
-        """
-        if self.generic:
-            return [self]
-        return list(
-            {
-                generic: None
-                for factor in self.factors_all
-                for generic in factor.generic_factors
-            }
-        )
-
-    def consistent_factor_groups(
-        self,
-        self_factors: Tuple[Factor],
-        other_factors: Tuple[Factor],
-        matches: Dict[Factor, Factor],
-    ):
-        """
-        Find whether two sets of :class:`.Factor`\s can be consistent.
-
-        Works by first determining whether one :class:`.Factor`
-        potentially :meth:`~.Factor.contradicts` another,
-        and then determining whether it's possible to make
-        context assignments match between the contradictory
-        :class:`.Factor`\s.
-
-        .. Note::
-            Does ``Factor: None`` in matches always mean that
-            the :class:`.Factor` can avoid being matched in a
-            contradictory way?
-
-        :returns:
-            whether unassigned context factors can be assigned in such
-            a way that there's no contradiction between any factor in
-            ``self_factors`` and ``other_factors``, given that some
-            :class:`.Factor`\s have already been assigned as
-            described by ``matches``.
-        """
-        for self_factor in self_factors:
-            for other_factor in other_factors:
-                if self_factor.contradicts(other_factor):
-                    if all(
-                        all(
-                            matches.get(key) == context_register[key]
-                            or matches.get(context_register[key] == key)
-                            for key in self_factor.generic_factors
-                        )
-                        for context_register in self_factor._context_registers(
-                            other_factor, means
-                        )
-                    ):
-                        return False
-        return True
-
-    def contradiction_between_outputs(
-        self, other: Procedure, matches: Dict[Factor, Factor]
-    ) -> bool:
-        """
-        Test whether outputs of two :class:`Procedure`\s can contradict.
-
-        :param other:
-            another :class:`Factor`
-
-        :param matches:
-            keys representing :class:`.Factor`\s in ``self`` and
-            values representing :class:`.Factor`\s in ``other``. The
-            keys and values have been found in corresponding positions
-            in ``self`` and ``other``.
-
-        :returns:
-            whether any :class:`.Factor` assignment can be found that
-            makes a :class:`.Factor` in the output of ``other`` contradict
-            a :class:`.Factor` in the output of ``self``.
-        """
-        for other_factor in other.outputs:
-            for self_factor in self.outputs:
-                if other_factor.contradicts(self_factor):
-                    generic_pairs = zip(
-                        self_factor.generic_factors, other_factor.generic_factors
-                    )
-                    if all(matches.get(pair[0]) == pair[1] for pair in generic_pairs):
-                        return True
-        return False
-
-    def contradicts(self, other) -> bool:
-        """
-        Find if ``self`` applying in some cases implies ``other`` cannot apply in some.
-
-        Raises an error because, by analogy with :meth:`.Procedure.implies`\,
-        users might expect this method to return ``True`` only when
-        :class:`ProceduralRule` with ``universal=False`` and Procedure ``self``
-        would contradict another :class:`ProceduralRule` with ``universal=False``
-        and Procedure ``other``. But that would never happen.
-        """
-        raise NotImplementedError(
-            "Procedures do not contradict one another unless one of them ",
-            "applies in 'ALL' cases. Consider using ",
-            "'Procedure.contradicts_some_to_all' or 'ProceduralRule.contradicts'.",
-        )
-
-    def contradicts_some_to_all(self, other: Procedure) -> bool:
-        """
-        Find if ``self`` applying in some cases implies ``other`` cannot apply in all.
-
-        :returns:
-            whether the assertion that ``self`` applies in
-            **some** cases contradicts that ``other`` applies in **all**
-            cases, where at least one of the :class:`.Rule`\s is ``mandatory``.
-        """
-        if not isinstance(other, self.__class__):
-            return False
-
-        self_despite_or_input = (*self.despite, *self.inputs)
-
-        # For self to contradict other, every input of other
-        # must be implied by some input or despite factor of self.
-        relations = (Analogy(other.inputs, self_despite_or_input, operator.le),)
-        matchlist = self._all_relation_matches(relations)
-
-        # For self to contradict other, some output of other
-        # must be contradicted by some output of self.
-
-        return any(self.contradiction_between_outputs(other, m) for m in matchlist)
-
-    def implies_all_to_all(self, other: Procedure) -> bool:
-        """
-        Find if ``self`` applying in all cases implies ``other`` applies in all.
-
-        ``self`` does not imply ``other`` if any output of ``other``
-        is not equal to or implied by some output of ``self``.
-
-        For ``self`` to imply ``other``, every input of ``self``
-        must be implied by some input of ``other``.
-
-        ``self`` does not imply ``other`` if any despite of ``other``
-        :meth:`~.Factor.contradicts` an input of ``self``.
-
-        :returns:
-            whether the assertion that ``self`` applies in **all** cases
-            implies that ``other`` applies in **all** cases.
-        """
-        if not isinstance(other, self.__class__):
-            return False
-
-        if self.means(other):
-            return True
-
-        relations = (
-            Analogy(other.outputs, self.outputs, operator.le),
-            Analogy(self.inputs, other.inputs, operator.le),
-        )
-        matchlist = self._all_relation_matches(relations)
-
-        # For every factor in other, find the permutations of entity slots
-        # that are consistent with matchlist and that don't cause the factor
-        # to contradict any factor of self.
-
-        return any(
-            self.consistent_factor_groups(self.inputs, other.despite, matches)
-            for matches in matchlist
-        )
+    May decide some aspect of current litigation, and also potentially
+    may be cided and reused by future courts. When :class:`Rule`\s appear as
+    judicial holdings they are often hypothetical and don't necessarily
+    imply that the court accepts the :class:`.Fact` assertions or other
+    :class:`.Factor`\s that make up the inputs or outputs of the
+    :class:`.Procedure` mentioned in the :class:`Rule`.
 
-    def implies(self, other: Procedure) -> bool:
-        """
-        Call :meth:`__ge__` as an alias.
+    :param procedure:
+        a :class:`.Procedure` containing the inputs, and despite
+        :class:`.Factor`\s and resulting outputs when this rule
+        is triggered.
 
-        :returns:
-            bool indicating whether ``self`` implies ``other``
-        """
-        return self >= other
+    :param enactments:
+        the :class:`.Enactment`\s cited as authority for
+        invoking the ``procedure``.
 
-    def implies_all_to_some(self, other: Procedure) -> bool:
-        """
-        Find if ``self`` applying in all cases implies ``other`` applies in some.
+    :param enactments_despite:
+        the :class:`.Enactment`\s specifically cited as failing
+        to preclude application of the ``procedure``.
 
-        For ``self`` to imply ``other``, every output of ``other``
-        must be equal to or implied by some output of ``self``.
+    :param mandatory:
+        whether the ``procedure`` is mandatory for the
+        court to apply whenever the :class:`.Rule` is properly invoked.
+        ``False`` means that the ``procedure`` is "discretionary".
 
-        For ``self`` to imply ``other``, every input of ``self`` must not be
-        contradicted by any input or despite of ``other``.
+    :param universal:
+        ``True`` if the ``procedure`` is applicable whenever
+        its inputs are present. ``False`` means that the ``procedure`` is
+        applicable in "some" situation where the inputs are present.
 
-        ``self`` does not imply ``other`` if any "despite" :class:`.Factor`\s
-        of ``other`` are not implied by inputs of ``self``.
+    :param rule_valid:
+        ``True`` means the :class:`Rule` is asserted to be valid (or
+        useable by a court in litigation). ``False`` means it's asserted
+        to be invalid.
 
-        :returns:
-            whether the assertion that ``self`` applies in **all** cases
-            implies that ``other`` applies in **some** cases (that is, if
-            the list of ``self``'s inputs is not considered an exhaustive list
-            of the circumstances needed to invoke the procedure).
-        """
+    :param decided:
+        ``False`` means that it should be deemed undecided
+        whether the :class:`Rule` is valid, and thus can have the
+        effect of overruling prior holdings finding the :class:`.Rule`
+        to be either valid or invalid. Seemingly, ``decided=False``
+        should render the ``rule_valid`` flag irrelevant. Note that
+        if an opinion merely says the court is not deciding whether
+        a :class:`.Rule` is valid, there is no holding, and no
+        :class:`.Rule` object should be created. Deciding not to decide
+        a :class:`Rule`\'s validity is not the same thing as deciding
+        that the :class:`.Rule` is undecided.
 
-        if not isinstance(other, self.__class__):
-            return False
+    :param generic:
+        whether the :class:`Rule` is being mentioned in a generic
+        context. e.g., if the :class:`Rule` is being mentioned in
+        an :class:`.Argument` object merely as an example of the
+        kind of :class:`Rule` that might be mentioned in such an
+        :class:`.Argument`.
 
-        if self.implies_all_to_all(other):
-            return True
+    :param name:
+        an identifier used to retrieve this :class:`Rule` when
+        needed for the composition of another :class:`.Factor`
+        object.
+    """
 
-        other_despite_or_input = (*other.despite, *other.inputs)
-        self_despite_or_input = (*self.despite, *self.inputs)
+    procedure: Optional[Procedure] = None
+    enactments: Union[Enactment, Iterable[Enactment]] = ()
+    enactments_despite: Union[Enactment, Iterable[Enactment]] = ()
+    mandatory: bool = False
+    universal: bool = False
+    rule_valid: bool = True
+    decided: bool = True
+    generic: bool = False
+    name: Optional[str] = None
+    outputs: Optional[Union[Factor, Iterable[Factor]]] = None
+    inputs: Optional[Union[Factor, Iterable[Factor]]] = None
+    despite: Optional[Union[Factor, Iterable[Factor]]] = None
 
-        relations = (
-            Analogy(other.outputs, self.outputs, operator.le),
-            Analogy(other.despite, self_despite_or_input, operator.le),
-        )
+    context_factor_names: ClassVar = ("procedure",)
+    directory: ClassVar = get_directory_path("holdings")
 
-        matchlist = self._all_relation_matches(relations)
+    def __post_init__(self):
+        for attr in ("enactments", "enactments_despite"):
+            value = self.__dict__[attr]
+            if isinstance(value, Enactment):
+                object.__setattr__(self, attr, self._wrap_with_tuple(value))
 
-        return any(
-            self.consistent_factor_groups(self.inputs, other_despite_or_input, matches)
-            for matches in matchlist
-        )
+        if self.procedure is None:
+            if self.outputs is None:
+                raise ValueError(
+                    "To construct a Rule you must specify either a Procedure "
+                    + "or output/input/despite Factors for use in constructing "
+                    + "a Procedure (including at least one output)."
+                )
+            object.__setattr__(
+                self,
+                "procedure",
+                Procedure(
+                    outputs=self.outputs, inputs=self.inputs, despite=self.despite
+                ),
+            )
+        object.__setattr__(self, "outputs", self.procedure.outputs)
+        object.__setattr__(self, "inputs", self.procedure.inputs)
+        object.__setattr__(self, "despite", self.procedure.despite)
 
-    def _implies_if_present(self, other: Factor) -> bool:
+    def __add__(self, other) -> Optional[Rule]:
         """
-        Find if ``self`` would imply ``other`` if they aren't absent.
-
-        When ``self`` and ``other`` are included in
-        :class:`Rule`\s that both apply in **some** cases:
-
-        ``self`` does not imply ``other`` if any input of ``other``
-        is not equal to or implied by some input of ``self``.
-
-        ``self`` does not imply ``other`` if any output of ``other``
-        is not equal to or implied by some output of self.
-
-        ``self`` does not imply ``other`` if any despite of ``other``
-        is not equal to or implied by some despite or input of ``self``.
+        Create new :class:`Rule` if ``self`` can satisfy the :attr:`inputs` of ``other``
 
-        :returns:
-            whether the assertion that ``self`` applies in some cases
-            implies that the :class:`.Procedure` ``other`` applies
-            in some cases.
-        """
+        If both ``self`` and ``other`` have False for :attr:`universal`,
+        or if either has ``False`` for :attr:`rule_valid'\,
+        then returns ``None``. Otherwise:
 
-        despite_or_input = (*self.despite, *self.inputs)
+        If the union of the :attr:`inputs` and :attr:`outputs` of ``self``
+        would trigger ``other``, then return a new version of ``self``
+        with the output :class:`.Factor`\s of ``other`` as well as the
+        outputs of ``self``.
 
-        relations = (
-            Analogy(other.outputs, self.outputs, operator.le),
-            Analogy(other.inputs, self.inputs, operator.le),
-            Analogy(other.despite, despite_or_input, operator.le),
-        )
+        The new ``universal`` and ``mandatory`` values are the
+        lesser of the old values for each.
 
-        return bool(self._all_relation_matches(relations))
+        Don't test whether ``self`` could be triggered by the outputs
+        of other. Let user do ``other + self`` for that.
 
-    def means(self, other) -> bool:
-        """
-        Determine whether ``other`` has the same meaning as ``self``.
+        :param other:
+            another :class:`Rule` to try to add to ``self``
 
         :returns:
-            whether the two :class:`.Procedure`\s have all the same
-            :class:`.Factor`\s with the same context factors in the
-            same roles.
-        """
-
-        if not isinstance(other, self.__class__):
-            return False
-
-            # Verifying that every factor in self is in other.
-            # Also verifying that every factor in other is in self.
-        groups = ("outputs", "inputs", "despite")
-        matchlist = [{}]
-        for group in groups:
-            updater = Analogy(
-                need_matches=self.__dict__[group],
-                available=other.__dict__[group],
-                comparison=means,
-            )
-            matchlist = updater.update_matchlist(matchlist)
-        if not bool(matchlist):
-            return False
-
-        # Now doing the same thing in reverse
-        matchlist = [{}]
-        for group in groups:
-            updater = Analogy(
-                need_matches=other.__dict__[group],
-                available=self.__dict__[group],
-                comparison=means,
+            a combined :class:`Rule` that extends the procedural
+            move made in ``self``, if possible. Otherwise ``None``.
+        """
+        if not isinstance(other, Rule):
+            raise TypeError
+        if self.rule_valid is False or other.rule_valid is False:
+            return None
+        if self.universal is False and other.universal is False:
+            return None
+
+        if not other.needs_subset_of_enactments(self):
+            return None
+
+        matchlist = self.procedure.triggers_next_procedure(other.procedure)
+        if matchlist:
+            # Arbitrarily choosing the first match to decide what
+            # generic Factors appear in the new outputs.
+            # Wouldn't it have been better to get just one match with a generator?
+            triggered_rule = other.new_context(matchlist[0])
+            new_procedure = self.procedure.evolve(
+                {"outputs": (*self.outputs, *triggered_rule.outputs)}
             )
-            matchlist = updater.update_matchlist(matchlist)
-        return bool(matchlist)
-
-    @new_context_helper
-    def new_context(self, changes: Dict[Factor, Factor]) -> Procedure:
-        """
-        Create new :class:`Procedure`, replacing keys of ``changes`` with values.
-
-        :param changes:
-            a :class:`dict` of :class:`.Factor`\s to replace
-            matched to the :class:`.Factor`\s that should replace them
-
-        :returns:
-            new :class:`.Procedure` object, replacing keys of
-            ``changes`` with their values.
-        """
-        new_dict = self.__dict__.copy()
-        for name in self.context_factor_names:
-            new_dict[name] = tuple(
-                [factor.new_context(changes) for factor in new_dict[name]]
+            return self.evolve(
+                {
+                    "procedure": new_procedure,
+                    "universal": min(self.universal, other.universal),
+                    "mandatory": min(self.mandatory, other.mandatory),
+                }
             )
-        return self.__class__(**new_dict)
-
-    @staticmethod
-    def _all_relation_matches(
-        relations: Tuple[Analogy, ...]
-    ) -> List[Dict[Factor, Optional[Factor]]]:
-        """
-        Find all context registers consistent with multiple :class:`.Analogy`\s.
-
-        :param relations:
-            a series of :class:`.Analogy` comparisons in which
-            the ``need_matches`` :class:`.Factor`\s all refer to
-            one context (for instance, the same :class:`.Opinion`),
-            and the ``available`` :class:`.Factor`\s all refer to
-            another context.
-
-        :returns:
-            a list of all context registers consistent with all of the
-            :class:`.Analogy`\s.
-        """
-        matchlist = [{}]
-        for relation in relations:
-            matchlist = relation.update_matchlist(matchlist)
-        return matchlist
-
-
-@dataclass(frozen=True)
-class Rule(Factor):
-    """
-    A statement of a legal doctrine which a court may posit as authoritative.
-
-    May decide some aspect of current litigation, and also potentially
-    may be cided and reused by future courts. When ``Rule``\s appear as
-    judicial holdings they are often hypothetical and don't necessarily
-    imply that the court accepts the :class:`.Fact` assertions or other
-    :class:`.Factor`\s that make up the inputs or outputs of the
-    :class:`Procedure` mentioned in the ``Rule``.
-    """
-
-    directory = get_directory_path("holdings")
+        return None
 
     @classmethod
     def collection_from_dict(
         cls,
         case: Dict,
         mentioned: Optional[List[Factor]] = None,
-        regime: Optional[Regime] = None
+        regime: Optional[Regime] = None,
     ) -> List[Rule]:
         """
         Create a :py:class:`list` of :class:`Rule`\s from JSON.
@@ -525,18 +214,28 @@ def collection_from_dict(
         """
         if not mentioned:
             mentioned: List[Factor] = []
-        factor_dicts=case.get("mentioned_factors")
+        factor_dicts = case.get("mentioned_factors")
         if factor_dicts:
             for factor_dict in factor_dicts:
-                _, mentioned = Factor.from_dict(factor_dict, mentioned=mentioned, regime=regime)
+                _, mentioned = Factor.from_dict(
+                    factor_dict, mentioned=mentioned, regime=regime
+                )
 
         finished_rules: List[Rule] = []
         for rule in case.get("holdings"):
-            # This will need to change for Attribution holdings
-            finished_rule, mentioned = ProceduralRule.from_dict(
+
+            # This basic formatting could be moved elsewhere, but
+            # it's needed before generating multiple rules based
+            # on the "exclusive" flag in the JSON.
+            for category in ("inputs", "despite", "outputs"):
+                if isinstance(rule.get(category), dict):
+                    rule[category] = [rule[category]]
+
+            for finished_rule, new_mentioned in Rule.from_dict(
                 rule, mentioned, regime=regime
-            )
-            finished_rules.append(finished_rule)
+            ):
+                finished_rules.append(finished_rule)
+                mentioned = new_mentioned
         return finished_rules
 
     @classmethod
@@ -547,14 +246,14 @@ def from_json(
         regime: Optional[Regime] = None,
     ) -> List[Rule]:
         """
-        Load a list of ``Rule``\s from JSON.
+        Load a list of :class:`Rule`\s from JSON.
 
         Does not cause an :class:`.Opinion` to :meth:`~.Opinion.posit`
-        the ``Rule``\s as holdings.
+        the :class:`Rule`\s as holdings.
 
         :param filename:
             the name of the JSON file to look in for :class:`Rule`
-            data in the format that lists ``mentioned_entities``
+            data in the format that lists ``mentioned_factors``
             followed by a list of holdings
 
         :param directory:
@@ -573,88 +272,86 @@ def from_json(
             case = json.load(f)
         return cls.collection_from_dict(case, regime=regime)
 
-@dataclass(frozen=True)
-class ProceduralRule(Rule):
-    """
-    A statement of a legal doctrine about a :class:`.Procedure` for litigation.
-
-    :param procedure:
-        a :class:`.Procedure` containing the inputs, and despite
-        :class:`.Factor`\s and resulting outputs when this rule
-        is triggered.
-
-    :param enactments:
-        the :class:`.Enactment`\s cited as authority for
-        invoking the ``procedure``.
-
-    :param enactments_despite:
-        the :class:`.Enactment`\s specifically cited as failing
-        to preclude application of the ``procedure``.
-
-    :param mandatory:
-        whether the ``procedure`` is mandatory for the
-        court to apply whenever the :class:`.Rule` is properly invoked.
-        ``False`` means that the ``procedure`` is "discretionary".
-
-    :param universal:
-        ``True`` if the ``procedure`` is applicable whenever
-        its inputs are present. ``False`` means that the ``procedure`` is
-        applicable in "some" situation where the inputs are present.
-
-    :param rule_valid:
-        ``True`` means the :class:`Rule` is asserted to be valid (or
-        useable by a court in litigation). ``False`` means it's asserted
-        to be invalid.
+    @classmethod
+    def contrapositive_from_dict(
+        cls, record: Dict, mentioned: List[Factor], regime: Optional[Regime] = None
+    ) -> Iterator[Tuple[Rule, List[Factor]]]:
+        """
+        Make contrapositive forms of a :class:`Rule` described by JSON input.
 
-    :param decided:
-        ``False`` means that it should be deemed undecided
-        whether the :class:`Rule` is valid, and thus can have the
-        effect of overruling prior holdings finding the :class:`.Rule`
-        to be either valid or invalid. Seemingly, ``decided=False``
-        should render the ``rule_valid`` flag irrelevant. Note that
-        if an opinion merely says the court is not deciding whether
-        a :class:`.Rule` is valid, there is no holding, and no
-        :class:`.Rule` object should be created. Deciding not to decide
-        a :class:`Rule`\'s validity is not the same thing as deciding
-        that the :class:`.Rule` is undecided.
+        If ``record`` contains the entry ``"exclusive": True``, that means
+        the specified :class:`~Rule.inputs`` are the only way to reach the specified
+        output. (Multiple :class:`~Rule.outputs` are not allowed when the ``exclusive``
+        flag is ``True``.) When that happens, it can be inferred that in the absence
+        of any of the inputs, the output must also be absent. So, this method will be
+        called once for each input to create more :class:`Rule`\s containing that
+        information. None of the completed :class:`Rule` objects will contain an
+        ``exclusive`` flag.
 
-    :param generic:
-        whether the :class:`Rule` is being mentioned in a generic
-        context. e.g., if the :class:`Rule` is being mentioned in
-        an :class:`.Argument` object merely as an example of the
-        kind of :class:`Rule` that might be mentioned in such an
-        :class:`.Argument`.
+        :param record:
+            a :class:`dict` derived from the JSON format that
+            lists ``mentioned_entities`` followed by a
+            series of :class:`Rule`\s. Only one of the :class:`Rule`\s
+            will by covered by this :class:`dict`.
 
-    :param name:
-        an identifier used to retrieve this :class:`Rule` when
-        needed for the composition of another :class:`.Factor`
-        object.
-    """
+        :param mentioned:
+            a series of context factors, including any generic
+            :class:`.Factor`\s that need to be mentioned in
+            :class:`.Predicate`\s. These will have been constructed
+            from the ``mentioned_entities`` section of the input
+            JSON.
 
-    procedure: Procedure
-    enactments: Union[Enactment, Iterable[Enactment]] = ()
-    enactments_despite: Union[Enactment, Iterable[Enactment]] = ()
-    mandatory: bool = False
-    universal: bool = False
-    rule_valid: bool = True
-    decided: bool = True
-    generic: bool = False
-    name: Optional[str] = None
-    context_factor_names: ClassVar = ("procedure",)
+        :param regime:
 
-    def __post_init__(self):
+        :returns:
+            iterator yielding :class:`Rule`\s with the items
+            from ``mentioned_entities`` as ``context_factors``
+        """
 
-        for attr in ("enactments", "enactments_despite"):
-            value = self.__dict__[attr]
-            if isinstance(value, Enactment):
-                object.__setattr__(self, attr, self._wrap_with_tuple(value))
+        if len(record["outputs"]) != 1:
+            raise ValueError(
+                "The 'exclusive' attribute is not allowed for Rules "
+                + "with more than one 'output' Factor. If the set of Factors "
+                + "in 'inputs' is really the only way to reach any of the "
+                + "'outputs', consider making a separate 'exclusive' entry "
+                + "for each output."
+            )
+        if record["outputs"][0].get("absent") is True:
+            raise ValueError(
+                "The 'exclusive' attribute is not allowed for Rules "
+                + "with an 'absent' 'output' Factor. This would indicate "
+                + "that the output can or must be present in every litigation "
+                + "unless specified inputs are present, which is unlikely."
+            )
+        if record.get("rule_valid") is False:
+            raise NotImplementedError(
+                "The ability to state that it is not 'valid' to assert "
+                + "that a Rule is the 'exclusive' way to reach an output is "
+                + "not implemented, so 'rule_valid' cannot be False while "
+                + "'exclusive' is True. Try expressing this in another way "
+                + "without the 'exclusive' keyword."
+            )
+        record["mandatory"] = not record.get("mandatory")
+        record["universal"] = not record.get("universal")
+        del record["exclusive"]
+        record["outputs"][0]["absent"] = True
+
+        for input_factor in record["inputs"]:
+            new_record = record.copy()
+            new_input = input_factor.copy()
+            new_input["absent"] = not new_input.get("absent")
+            new_record["inputs"] = [new_input]
+            for new_tuple in Rule.from_dict(
+                record=new_record, mentioned=mentioned, regime=regime
+            ):
+                yield new_tuple
 
     @classmethod
     def from_dict(
-        cls, record: Dict, mentioned: List[Factor], regime: Optional["Regime"] = None
-    ) -> Tuple[ProceduralRule, List[Factor]]:
+        cls, record: Dict, mentioned: List[Factor], regime: Optional[Regime] = None
+    ) -> Iterator[Tuple[Rule, List[Factor]]]:
         """
-        Make :class:`Rule` from a :py:class:`dict` of strings and a list of mentioned :class:`.Factor`\s.
+        Make :class:`Rule` from a :class:`dict` of strings and a list of mentioned :class:`.Factor`\s.
 
         :param record:
             a :class:`dict` derived from the JSON format that
@@ -669,8 +366,10 @@ def from_dict(
             from the ``mentioned_entities`` section of the input
             JSON.
 
+        :param regime:
+
         :returns:
-            a :class:`list` of :class:`Rule`\s with the items
+            iterator yielding :class:`Rule`\s with the items
             from ``mentioned_entities`` as ``context_factors``
         """
 
@@ -678,7 +377,7 @@ def list_from_records(
             record_list: Union[Dict[str, str], List[Dict[str, str]]],
             mentioned: List[Factor],
             class_to_create,
-            regime: Optional["Regime"] = None
+            regime: Optional[Regime] = None,
         ) -> Tuple[Union[Factor, Enactment]]:
             factors_or_enactments: List[Union[Factor, Enactment]] = []
             if not isinstance(record_list, list):
@@ -707,8 +406,8 @@ def list_from_records(
             despite=factor_groups["despite"],
         )
 
-        return (
-            ProceduralRule(
+        yield (
+            Rule(
                 procedure=procedure,
                 enactments=enactment_groups["enactments"],
                 enactments_despite=enactment_groups["enactments_despite"],
@@ -720,6 +419,10 @@ def list_from_records(
             mentioned,
         )
 
+        if record.get("exclusive") is True:
+            for response in Rule.contrapositive_from_dict(record, mentioned, regime):
+                yield response
+
     @property
     def context_factors(self) -> Tuple:
         """
@@ -730,16 +433,6 @@ def context_factors(self) -> Tuple:
         """
         return self.procedure.context_factors
 
-    @property
-    def despite(self):
-        """
-        Call :class:`Procedure`\'s :meth:`~Procedure.despite` method.
-
-        :returns:
-            despite :class:`.Factor`\s from ``self``'s :class:`Procedure`
-        """
-        return self.procedure.despite
-
     @property
     def generic_factors(self) -> List[Optional[Factor]]:
         """
@@ -752,26 +445,6 @@ def generic_factors(self) -> List[Optional[Factor]]:
             return [self]
         return self.procedure.generic_factors
 
-    @property
-    def inputs(self):
-        """
-        Call :class:`Procedure`\'s :meth:`~Procedure.inputs` method.
-
-        :returns:
-            input :class:`.Factor`\s from ``self``'s :class:`Procedure`
-        """
-        return self.procedure.inputs
-
-    @property
-    def outputs(self):
-        """
-        Call :class:`Procedure`\'s :meth:`~Procedure.outputs` method.
-
-        :returns:
-            output :class:`.Factor`\s from ``self``'s :class:`Procedure`
-        """
-        return self.procedure.outputs
-
     def contradicts(self, other) -> bool:
         """
         Test if ``self`` :meth:`~.Factor.implies` ``other`` :meth:`~.Factor.negated`\.
@@ -783,11 +456,11 @@ def contradicts(self, other) -> bool:
         whether the holdings ``self`` and ``other`` assert that
         rules are decided or undecided.
 
-        A ``decided`` :class:`ProceduralRule` can never contradict
+        A ``decided`` :class:`Rule` can never contradict
         a previous statement that any :class:`Rule` was undecided.
 
         If rule A implies rule B, then a holding that B is undecided
-        contradicts a prior :class:`ProceduralRule` deciding that
+        contradicts a prior :class:`Rule` deciding that
         rule A is valid or invalid.
 
         :returns:
@@ -855,7 +528,7 @@ def __ge__(self, other) -> bool:
 
         :returns:
             whether ``self`` implies ``other``, which requires ``other``
-            to be another :class:`ProceduralRule`.
+            to be another :class:`Rule`.
         """
 
         if not isinstance(other, self.__class__):
@@ -884,12 +557,12 @@ def _implies_if_decided(self, other) -> bool:
         Test if ``self`` implies ``other`` if they're both decided.
 
         This is a partial version of the
-        :meth:`ProceduralRule.__ge__` implication function.
+        :meth:`Rule.__ge__` implication function.
 
         :returns:
             whether ``self`` implies ``other``, assuming that
             ``self.decided == other.decided == True`` and that
-            ``self`` and ``other`` are both :class:`ProceduralRule`\s,
+            ``self`` and ``other`` are both :class:`Rule`\s,
             although ``rule_valid`` can be ``False``.
         """
 
@@ -906,17 +579,14 @@ def _implies_if_decided(self, other) -> bool:
 
         return self._contradicts_if_valid(other)
 
-    def _implies_if_valid(self, other) -> bool:
+    def needs_subset_of_enactments(self, other) -> bool:
         """
-        Test if ``self`` implies ``other`` if they're valid and decided.
+        Test whether ``self``\'s :class:`.Enactment` support is a subset of ``other``\'s.
 
-        This is a partial version of the
-        :meth:`ProceduralRule.__ge__` implication function.
+        A :class:`Rule` makes a more powerful statement if it relies on
+        fewer :class:`.Enactment`\s (or applies despite more :class:`.Enactment`\s).
 
-        :returns:
-            whether ``self`` implies ``other``, assuming that
-            both are :class:`ProceduralRule`\s, and
-            ``rule_valid`` and ``decided`` are ``True`` for both of them.
+        So this method must return ``True`` for ``self`` to imply ``other``.
         """
 
         if not all(
@@ -929,6 +599,23 @@ def _implies_if_valid(self, other) -> bool:
             for other_d in other.enactments_despite
         ):
             return False
+        return True
+
+    def _implies_if_valid(self, other) -> bool:
+        """
+        Test if ``self`` implies ``other`` if they're valid and decided.
+
+        This is a partial version of the
+        :meth:`Rule.__ge__` implication function.
+
+        :returns:
+            whether ``self`` implies ``other``, assuming that
+            both are :class:`Rule`\s, and
+            ``rule_valid`` and ``decided`` are ``True`` for both of them.
+        """
+
+        if not self.needs_subset_of_enactments(other):
+            return False
 
         if other.mandatory > self.mandatory:
             return False
@@ -955,12 +642,12 @@ def __len__(self):
 
         return len(self.procedure)
 
-    def means(self, other: ProceduralRule) -> bool:
+    def means(self, other: Rule) -> bool:
         """
         Test whether ``other`` has the same meaning as ``self``.
 
         :returns:
-            whether ``other`` is a :class:`ProceduralRule` with the
+            whether ``other`` is a :class:`Rule` with the
             same meaning as ``self``.
         """
 
diff --git a/authorityspoke/selectors.py b/authorityspoke/selectors.py
index d3b1047f..b8080828 100644
--- a/authorityspoke/selectors.py
+++ b/authorityspoke/selectors.py
@@ -7,6 +7,7 @@
 from __future__ import annotations
 
 import json
+import re
 from dataclasses import dataclass
 
 from typing import Optional, Union
@@ -57,13 +58,18 @@ def __post_init__(self):
         if self.path and self.path.endswith("/"):
             object.__setattr__(self, "path", self.path.rstrip("/"))
 
+        if self.source and not self.path:
+            object.__setattr__(self, "exact", self.source.uri)
+
         if not self.exact:
             if self.source:
-                self.set_exact_from_source(self.source)
+                object.__setattr__(
+                    self, "exact", self.set_exact_from_source(self.source)
+                )
 
         object.__delattr__(self, "source")
 
-    def set_exact_from_source(self, source: Union[Regime, Code]) -> None:
+    def set_exact_from_source(self, source: Union[Regime, Code]) -> Optional[str]:
         """Use text found in ``source`` as ``exact`` parameter for ``self``."""
         if source.__class__.__name__ == "Regime":
             code = source.get_code(self.path)
@@ -72,8 +78,8 @@ def set_exact_from_source(self, source: Union[Regime, Code]) -> None:
         else:
             return None
 
-        selection = code.select_text(self)
-        object.__setattr__(self, "exact", self.exact_from_ends(selection))
+        section_text = code.section_text(self.path)
+        return self.exact_from_ends(section_text)
 
     def exact_from_ends(self, text: str) -> str:
         """
@@ -101,7 +107,7 @@ def exact_from_ends(self, text: str) -> str:
             right_end = None
         if right_end == -1:
             raise ValueError(f"'suffix' value '{self.suffix}' not found in '{text}'")
-        return text[left_end:right_end]
+        return text[left_end:right_end].strip()
 
     @property
     def json(self):
@@ -122,3 +128,10 @@ def json(self):
                 },
             }
         )
+
+    @property
+    def passage_regex(self):
+        """Get a regex to identify the selected text."""
+        prefix = (re.escape(self.prefix) + r"\s*") if self.prefix else ""
+        suffix = (r"\s*" + re.escape(self.suffix)) if self.suffix else ""
+        return (prefix + r"(" + re.escape(self.exact) + r")" + suffix).strip()
diff --git a/changelog.md b/changelog.md
index 3bcd957f..3ec73a91 100644
--- a/changelog.md
+++ b/changelog.md
@@ -5,10 +5,14 @@ Changelog
 dev
 ===
 
+- Merge ProceduralRule class with Rule
+- Ignore was/were differences in Predicate content
+- Let input JSON label a Rule as the "exclusive" way to get output
+- Create addition operator for Factors and Rules
 
 0.1.0 (2019-06-10)
 ==================
 
-- Added Regime and Jurisdiction classes to organize Enactments
-- Added TextQuoteSelector class to select text from Enactments
-- changed Enactment init method to use TextQuoteSelectors
+- Add Regime and Jurisdiction classes to organize Enactments
+- Add TextQuoteSelector class to select text from Enactments
+- Change Enactment init method to use TextQuoteSelectors
diff --git a/docs/api/context.rst b/docs/api/context.rst
index c517e122..967c29bb 100644
--- a/docs/api/context.rst
+++ b/docs/api/context.rst
@@ -11,3 +11,5 @@
 
 .. autoclass:: authorityspoke.relations.Analogy
     :members:
+
+.. autofunction:: authorityspoke.relations.all_analogy_matches
diff --git a/docs/api/factors.rst b/docs/api/factors.rst
index 99f58e61..71c86216 100644
--- a/docs/api/factors.rst
+++ b/docs/api/factors.rst
@@ -65,11 +65,11 @@
 ``Procedures``
 ==============
 
-.. inheritance-diagram:: authorityspoke.rules.Procedure
+.. inheritance-diagram:: authorityspoke.procedures.Procedure
     :top-classes: authorityspoke.factors.Factor
     :parts: 1
 
-.. autoclass:: authorityspoke.rules.Procedure
+.. autoclass:: authorityspoke.procedures.Procedure
     :members:
     :special-members:
     :private-members:
@@ -84,14 +84,3 @@
 .. autoclass:: authorityspoke.rules.Rule
     :members:
     :special-members:
-
-``ProceduralRules``
----------------------
-
-.. inheritance-diagram:: authorityspoke.rules.ProceduralRule
-    :top-classes: authorityspoke.factors.Factor
-    :parts: 1
-
-.. autoclass:: authorityspoke.rules.ProceduralRule
-    :members:
-    :special-members:
diff --git a/example_data/holdings/holding_feist.json b/example_data/holdings/holding_feist.json
new file mode 100644
index 00000000..60a0f821
--- /dev/null
+++ b/example_data/holdings/holding_feist.json
@@ -0,0 +1,249 @@
+{
+    "mentioned_factors": [
+
+        {
+            "type": "entity",
+            "name": "Rural Telephone Service Company"
+        },
+        {
+            "type": "entity",
+            "name": "Rural's telephone directory"
+        },
+        {
+            "type": "entity",
+            "name": "Rural's telephone listings",
+            "plural": true
+        },
+        {
+            "type": "entity",
+            "name": "Rural's arragement of its telephone listings"
+        },
+        {
+            "type": "entity",
+            "name": "Feist Publications"
+        },
+        {
+            "type": "entity",
+            "name": "Feist's telephone directory"
+        }
+    ],
+    "holdings": [{
+            "inputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was a fact"
+            },
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was copyrightable",
+                "truth": false
+            },
+            "enactments": [{
+                    "path": "/us/const/article-I/8/8",
+                    "exact": "To promote the Progress of Science and useful Arts, by securing for limited Times to Authors",
+                    "name": "securing for authors"
+                }, {
+                    "path": "/us/const/article-I/8/8",
+                    "exact": "the exclusive Right to their respective Writings",
+                    "name": "right to writings"
+                }, {
+                    "path": "/us/usc/t17/s103/b",
+                    "exact": "The copyright in a compilation",
+                    "name": "copyright in a compilation"
+                },
+                {
+                    "path": "/us/usc/t17/s103/b",
+                    "exact": "extends only to the material contributed by the author of such work, as distinguished from the preexisting material employed in the work, and does not imply any exclusive right in the preexisting material.",
+                    "name": "preexisting material provision"
+                }
+            ],
+            "mandatory": true,
+            "universal": true
+        },
+        {
+            "inputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was a compilation of facts"
+            },
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was copyrightable"
+            },
+            "enactments": ["securing for authors", "right to writings"],
+            "mandatory": true
+        },
+        {
+            "inputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was an idea"
+            },
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was copyrightable",
+                "truth": false
+            },
+            "enactments": ["securing for authors", "right to writings"],
+            "mandatory": true,
+            "universal": true
+        },
+        {
+            "inputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was original"
+            },
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was copyrightable"
+            },
+            "exclusive": true,
+            "enactments": ["securing for authors", "right to writings"]
+        },
+        {
+            "inputs": [{
+                    "type": "fact",
+                    "content": "Rural's telephone directory was independently created by Rural Telephone Service Company, as opposed to copied from other works"
+                },
+                {
+                    "type": "fact",
+                    "content": "Rural's telephone directory possessed at least some minimal degree of creativity"
+                }
+            ],
+            "despite": {
+                "type": "fact",
+                "content": "Rural's telephone directory closely resembled other works"
+            },
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was original"
+            },
+            "enactments": ["securing for authors", "right to writings"],
+            "universal": true
+        },
+        {
+            "inputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was a fact"
+            },
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was original",
+                "truth": false
+            },
+            "enactments": ["securing for authors", "right to writings"],
+            "mandatory": true,
+            "universal": true
+        },
+        {
+            "inputs": [{
+                    "type": "fact",
+                    "content": "Rural's telephone directory was a compilation of facts"
+                },
+                {
+                    "type": "fact",
+                    "content": "Rural Telephone Service Company was the creator of Rural's telephone directory"
+                },
+                {
+                    "type": "fact",
+                    "content": "Rural Telephone Service Company independently used a degree of creativity to choose which facts to include in Rural's telephone directory"
+                },
+                {
+                    "type": "fact",
+                    "content": "Rural Telephone Service Company independently used a degree of creativity to choose in what order to place the facts in Rural's telephone directory, and how to arrange the collected data so that they could be used effectively by readers"
+                }
+            ],
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was original"
+            },
+            "enactments": ["securing for authors", "right to writings"]
+        },
+        {
+            "inputs": [{
+                    "type": "fact",
+                    "content": "Rural's telephone directory was a compilation of facts"
+                },
+                {
+                    "type": "fact",
+                    "content": "Rural's telephone directory contained written expression protectable by copyright",
+                    "truth": false
+                }
+            ],
+            "despite": [{
+                    "type": "fact",
+                    "content": "Rural Telephone Service Company independently used a degree of creativity to choose which facts to include in Rural's telephone directory"
+                },
+                {
+                    "type": "fact",
+                    "content": "Rural Telephone Service Company independently used a degree of creativity to choose in what order to place the facts in Rural's telephone directory, and how to arrange the collected data so that they could be used effectively by readers"
+                }
+            ],
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was copyrightable",
+                "truth": false
+            },
+            "enactments": ["securing for authors", "right to writings"],
+            "rule_valid": false
+        },
+        {
+            "inputs": [{
+                "type": "fact",
+                "content": "Rural's telephone directory was a compilation of facts"
+            }, {
+                "type": "fact",
+                "content": "Feist's telephone directory did not feature the same selection and arrangement of facts as Rural's telephone directory"
+            }],
+            "despite": [{
+                "type": "fact",
+                "content": "Rural's telephone directory was copyrightable"
+            }, {
+                "type": "fact",
+                "content": "Feist Publications freely copied the facts contained in Rural's telephone directory, in preparing Feist's telephone directory"
+            }],
+            "outputs": {
+                "type": "fact",
+                "content": "Feist Publications infringed the copyright on Rural's telephone directory",
+                "truth": false
+            },
+            "enactments": ["securing for authors", "right to writings"]
+        },
+        {
+            "inputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was a compilation of facts"
+            },
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone directory was copyrightable",
+                "truth": false
+            },
+            "enactments": ["securing for authors", "right to writings", "copyright in a compilation", "preexisting material provision"]
+        },
+        {
+            "inputs": {
+                "type": "fact",
+                "content": "Rural's telephone listings were names, towns, and telephone numbers of telephone subscribers"
+            },
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's telephone listings were original",
+                "truth": false
+            },
+            "enactments": ["securing for authors", "right to writings", "copyright in a compilation", "preexisting material provision"]
+        },
+        {
+            "inputs": [{
+                "type": "fact",
+                "content": "Rural's telephone directory was a telephone directory."
+            }, {
+                "type": "fact",
+                "content": "Rural's arragement of its telephone listings was the method of listing subscribers alphabetically by surname in Rural's telephone directory"
+            }],
+            "outputs": {
+                "type": "fact",
+                "content": "Rural's arragement of its telephone listings was original",
+                "truth": false
+            },
+            "enactments": ["securing for authors", "right to writings", "copyright in a compilation", "preexisting material provision"]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/example_data/holdings/holding_lotus.json b/example_data/holdings/holding_lotus.json
index 5da1564c..b672e9c7 100644
--- a/example_data/holdings/holding_lotus.json
+++ b/example_data/holdings/holding_lotus.json
@@ -40,35 +40,8 @@
                 "path": "/us/usc/t17/s102/a",
                 "suffix": "Works of authorship include",
                 "name": "copyright protection"
-            }]
-        },
-        {
-            "inputs": [{
-                "type": "fact",
-                "content": "the Lotus menu command hierarchy was copyrightable",
-                "absent": true
             }],
-            "outputs": [{
-                "type": "fact",
-                "content": "Borland International infringed the copyright in the Lotus menu command hierarchy",
-                "absent": true
-            }],
-            "universal": true,
-            "enactments": ["copyright protection"]
-        },
-        {
-            "inputs": [{
-                "type": "fact",
-                "content": "Borland International copied constituent elements of the Lotus menu command hierarchy that were original",
-                "absent": true
-            }],
-            "outputs": [{
-                "type": "fact",
-                "content": "Borland International infringed the copyright in the Lotus menu command hierarchy",
-                "absent": true
-            }],
-            "universal": true,
-            "enactments": ["copyright protection"]
+            "exclusive": true
         },
         {
             "inputs": [{
diff --git a/example_data/opinions/feist_h.json b/example_data/opinions/feist_h.json
new file mode 100644
index 00000000..bcc81879
--- /dev/null
+++ b/example_data/opinions/feist_h.json
@@ -0,0 +1,52 @@
+{
+    "id": 11318883,
+    "url": "https://api.case.law/v1/cases/11318883/",
+    "frontend_url": "https://cite.case.law/us/499/340/",
+    "name": "FEIST PUBLICATIONS, INC. v. RURAL TELEPHONE SERVICE CO., INC.",
+    "name_abbreviation": "Feist Publications, Inc. v. Rural Telephone Service Co.",
+    "decision_date": "1991-03-27",
+    "docket_number": "No. 89-1909",
+    "first_page": "340",
+    "last_page": "364",
+    "citations": [{
+        "type": "official",
+        "cite": "499 U.S. 340"
+    }],
+    "volume": {
+        "url": "https://api.case.law/v1/volumes/32044057487944/",
+        "volume_number": "499"
+    },
+    "reporter": {
+        "url": "https://api.case.law/v1/reporters/983/",
+        "full_name": "United States Reports"
+    },
+    "court": {
+        "url": "https://api.case.law/v1/courts/us/",
+        "id": 9009,
+        "slug": "us",
+        "name": "Supreme Court of the United States",
+        "name_abbreviation": "U.S."
+    },
+    "jurisdiction": {
+        "url": "https://api.case.law/v1/jurisdictions/us/",
+        "id": 39,
+        "slug": "us",
+        "name": "U.S.",
+        "name_long": "United States",
+        "whitelisted": false
+    },
+    "casebody": {
+        "data": {
+            "attorneys": ["Kyler Knobbe argued the cause and filed briefs for petitioner.", "James M. Caplinger, Jr., argued the cause and filed a brief for respondent. "],
+            "parties": ["FEIST PUBLICATIONS, INC. v. RURAL TELEPHONE SERVICE CO., INC."],
+            "opinions": [{
+                "text": "Justice O’Connor\ndelivered the opinion of the Court.\nThis case requires us to clarify the extent of copyright protection available to telephone directory white pages.\ni — i\nRural Telephone Service Company, Inc., is a certified public utility that provides telephone service to several communities in northwest Kansas. It is subject to a state regulation that requires all telephone companies operating in Kansas to issue annually an updated telephone directory. Accordingly, as a condition of its monopoly franchise, Rural publishes a typical telephone directory, consisting of white pages and yellow pages. The white pages list in alphabetical order the names of Rural’s subscribers, together with their towns and telephone numbers. The yellow pages list Rural’s business subscribers alphabetically by category and feature classified advertisements of various sizes. Rural distributes its directory free of charge to its subscribers, but earns revenue by selling yellow pages advertisements.\nFeist Publications, Inc., is a publishing company that specializes in area-wide telephone directories. Unlike a typical directory, which covers only a particular calling area, Feist’s area-wide directories cover a much larger geographical range, reducing the need to call directory assistance or consult multiple directories. The Feist directory that is the subject of this litigation covers 11 different telephone service areas in 15 counties and contains 46,878 white pages listings — compared to Rural’s approximately 7,700 listings. Like Rural’s directory, Feist’s is distributed free of charge and includes both white pages and yellow pages. Feist and Rural compete vigorously for yellow pages advertising.\nAs the sole provider of telephone service in its service area, Rural obtains subscriber information quite easily. Persons desiring telephone service must apply to Rural and provide their names and addresses; Rural then assigns them a telephone number. Feist is not a telephone company, let alone one with monopoly status, and therefore lacks independent access to any subscriber information. To obtain white pages listings for its area-wide directory, Feist approached each of the 11 telephone companies operating in northwest Kansas and offered to pay for the right to use its white pages listings.\nOf the 11 telephone companies, only Rural refused to license its listings to Feist. Rural’s refusal created a problem for Feist, as omitting these listings would have left a gaping hole in its area-wide directory, rendering it less attractive to potential yellow pages advertisers. In a decision subsequent to that which we review here, the District Court determined that this was precisely the reason Rural refused to license its listings. The refusal was motivated by an unlawful purpose “to extend its monopoly in telephone service to a monopoly in yellow pages advertising.” Rural Telephone Service Co. v. Feist Publications, Inc., 737 F. Supp. 610, 622 (Kan. 1990).\nUnable to license Rural’s white pages listings, Feist used them without Rural’s consent. .Feist began by removing several thousand listings that fell outside the geographic range of its area-wide directory, then hired personnel to investigate the 4,935 that remained. These employees veri-fled the data reported by Rural and sought to obtain additional information. As a result, a typical Feist listing includes the individual’s street address; most of Rural’s listings do not. Notwithstanding these additions, however, 1,309 of the 46,878 listings in Feist’s 1983 directory were identical to listings in Rural’s 1982-1983 white pages. App. 54 (¶ 15-16), 57. Four of these were fictitious listings that Rural had inserted into its directory to detect copying.\nRural sued for copyright infringement in the District Court for the District of Kansas taking the position that Feist, in compiling its own directory, could not use the information contained in Rural’s white pages. Rural asserted that Feist’s employees were obliged to travel door-to-door or conduct a telephone survey to discover the same information for themselves. Feist responded that such efforts were economically impractical and, in any event, unnecessary because the information copied was beyond the scope of copyright protection. The District Court granted summary judgment to Rural, explaining that “[c]ourts have consistently held that telephone directories are copyrightable” and citing a string of lower court decisions. 663 F. Supp. 214, 218 (1987). In an unpublished opinion, the Court of Appeals for the Tenth Circuit affirmed “for substantially the reasons given by the district court.” App. to Pet. for Cert. 4a, judgt. order reported at 916 F. 2d 718 (1990). We granted certiorari, 498 U. S. 808 (1990), to determine whether the copyright in Rural’s directory protects the names, towns, and telephone numbers copied by Feist.\nII\nA\nThis case concerns the interaction of two well-established propositions. The first is that facts are not copyrightable; the other, that compilations of facts generally are. Each of these propositions possesses an impeccable pedigree. That there can be no valid copyright in facts is universally understood. The most fundamental axiom of copyright law is that “[n]o author may copyright his ideas or the facts he narrates.” Harper & Row, Publishers, Inc. v. Nation Enterprises, 471 U. S. 539, 556 (1985). Rural wisely concedes this point, noting in its brief that “[f]acts and discoveries, of course, are not themselves subject to copyright protection.” Brief for Respondent 24. At the same time, however, it is beyond dispute that compilations of facts are within the subject matter of copyright. Compilations were expressly mentioned in the Copyright Act of 1909, and again in the Copyright Act of 1976.\nThere is an undeniable tension between these two propositions. Many compilations consist of nothing but raw data— i. e., wholly factual information not accompanied by any original written expression. On what basis may one claim a copyright in such a work? Common sense tells us that 100 uncopyrightable facts do not magically change their status when gathered together in one place. Yet copyright law seems to contemplate that compilations that consist exclusively of facts are potentially within its scope.\nThe key to resolving the tension lies in understanding why facts are not copyrightable. The sine qua non of copyright is originality. To qualify for copyright protection, a work must be original to the author. See Harper & Row, supra, at 547-549. Original, as the term is used in copyright, means only that the work was independently created by the author (as opposed to copied from other works), and that it possesses at least some minimal degree of creativity. 1 M. Nimmer & D. Nimmer, Copyright §§2.01[A], [B] (1990) (hereinafter Nimmer). To be sure, the requisite level of creativity is extremely low; even a slight amount will suffice. The vast majority of works make the grade quite easily, as they posséss some creative spark, “no matter how crude, humble or obvious” it might be. Id., § 1.08[C][1]. Originality does not signify novelty; a work may be original even though it closely resembles other works so long as the similarity is fortuitous, not the result of copying. To illustrate, assume that two poets, each ignorant of the other, compose identical poems. Neither work is novel, yet both are original and, hence, copyrightable. See Sheldon v. Metro-Goldwyn Pictures Corp., 81 F. 2d 49, 54 (CA2 1936).\nOriginality is a constitutional requirement. The source of Congress’ power to enact copyright laws is Article I, § 8, cl. 8, of the Constitution, which authorizes Congress to “secur[e] for limited Times to Authors . . . the. exclusive Right to their respective Writings.” In two decisions from the late 19th century — The Trade-Mark Cases, 100 U. S. 82 (1879); and Burrow-Giles Lithographic Co. v. Sarony, 111 U. S. 53 (1884)—this Court defined the crucial terms “authors” and “writings.” In so doing, the Court made it unmistakably clear that these terms presuppose a degree of originality.\nIn The Trade-Mark Cases, the Court addressed the constitutional scope of “writings.” For a particular work to be classified “under the head of writings of authors,” the Court determined, “originality is required.” 100 U. S., at 94. The Court explained that originality requires independent creation plus a modicum of creativity: “[W]hile the word writings may be liberally construed, as it has been, to include original designs for engraving, prints, &c., it is only such as are original, and are founded in the creative powers of the mind. The writings which are to be protected are the fruits of intellectual labor, embodied in the form of books, prints, engravings, and the like.” Ibid, (emphasis in original).\nIn Burrow-Giles, the Court distilled the same requirement from the Constitution’s use of the word “authors.” The Court defined “author,” in a constitutional sense, to mean “he to whom anything owes its origin; originator; maker.” 111 U. S., at 58 (internal quotation marks omitted). As in The Trade-Mark Cases, the Court emphasized the creative component of originality. It described copyright as being limited to “original intellectual conceptions of the author,” 111 U. S., at 58, and stressed the importance of requiring an author who accuses another of infringement to prove “the existence of those facts of originality, of intellectual production, of thought, and conception.” Id., at 59-60.\nThe originality requirement articulated in The Trade-Mark Cases and Burrow-Giles remains the touchstone of copyright protection today. See Goldstein v. California, 412 U. S. 546, 561-562 (1973). It is the very “premise of copyright law.” Miller v. Universal City Studios, Inc., 650 F. 2d 1365, 1368 (CA5 1981). Leading scholars agree on this point. As one pair of commentators succinctly puts it: “The originality requirement is constitutionally mandated for all works.” Patterson & Joyce, Monopolizing the Law: The Scope of Copyright Protection for Law Reports and Statutory Compilations, 36 UCLA L. Rev. 719, 763, n. 155 (1989) (emphasis in original) (hereinafter Patterson & Joyce). Accord, id., at 759-760, and n. 140; Nimmer § 1.06[A] (“[Originality is a statutory as well as a constitutional requirement”); id., §1.08[C][1] (“[A] modicum of intellectual labor . . . clearly constitutes an essential constitutional element”).\nIt is this bedrock principle of copyright that mandates the law’s seemingly disparate treatment of facts and factual compilations. “No one may claim originality as to facts.” Id., §2.11[A], p. 2-157. This is because facts do not owe their origin to an act of authorship. The distinction is one between creation and discovery: The first person to find and report a particular fact has not created the fact; he or she has merely discovered its existence. To borrow from Burrow-Giles, one who discovers a fact is not its “maker” or “originator.” 111 U. S., at 58. “The discoverer merely finds and records.” Nimmer §2.03[E]. Census takers, for example, do not “create” the population figures that emerge from their efforts; in a sense, they copy these figures from the world around them. Denicola, Copyright in Collections of Facts: A Theory for the Protection of Nonfiction Literary Works, 81 Colum. L. Rev. 516, 525 (1981) (hereinafter Denicola). Census data therefore do not trigger copyright because these data are not “original” in the constitutional sense. Nimmer § 2.03[E]. The same is true of all facts —scientific, historical, biographical, and news of the day. “[T]hey may not be copyrighted and are part of the public domain available to every person.” Miller, supra, at 1369.\nFactual compilations, on the other hand, may possess the requisite originality. The compilation author typically chooses which facts to include, in what order to place them, and how to arrange the collected data so that they may be used effectively by readers. These choices as to selection and arrangement, so long as they are made independently by the compiler and entail a minimal degree of creativity, are sufficiently original that Congress may protect such compilations through the copyright laws. Nimmer §§2.11[D], 3.03; Denicola 523, n. 38. Thus, even a directory that contains absolutely no protectible written expression, only facts, meets the constitutional minimum for copyright protection if it features an original selection or arrangement. See Harper & Row, 471 U. S., at 547. Accord, Nimmer §3.03.\nThis protection is subject to an important limitation. The mere fact that a work is copyrighted does not mean that every element of the work may be protected. Originality remains the sine qua non of copyright; accordingly, copyright protection may extend only to those components of a work that are original to the author. Patterson & Joyce 800-802; Ginsburg, Creation and Commercial Value: Copyright Protection of Works of Information, 90 Colum. L. Rev. 1865, 1868, and n. 12 (1990) (hereinafter Ginsburg). Thus, if the compilation author clothes facts with an original collocation of words, he or she may be able to claim a copyright in this written expression. Others may copy the underlying facts from the publication, but not the precise words used to present them. In Harper & Row, for example, we explained that President Ford could not prevent others from copying bare historical facts from his autobiography, see 471 U. S., at 556-557, but that he could prevent others from copying his “subjective descriptions and portraits of public figures.” Id., at 563. Where the compilation author adds no written expression but rather lets the facts speak for themselves, the expressive element is more elusive. The only conceivable expression is the manner in which the compiler has selected and arranged the facts. Thus, if the selection and arrangement are original, these elements of the work are eligible for copyright protection. See Patry, Copyright in Compilations of Facts (or Why the “White Pages” Are Not Copyrightable), 12 Com. & Law 37, 64 (Dec. 1990) (hereinafter Patry). No matter how original the format, however, the facts themselves do not become original through association. See Patterson & Joyce 776.\nThis inevitably means that the copyright in a factual compilation is thin. Notwithstanding a valid copyright, a subsequent compiler remains free to use the facts contained in another’s publication to aid in preparing a competing work, so long as the competing work does not feature the same selection and arrangement. As one commentator explains it: “[N]o matter how much original authorship the work displays, the facts and ideas it exposes are free for the taking .... [T]he very same facts and ideas may be divorced from the context imposed by the author, and restated or reshuffled by second comers, even if the author was the first to discover the facts or to propose the ideas.” Ginsburg 1868.\nIt may seem unfair that much of the fruit of the compiler’s labor may be used by others without compensation. As Justice Brennan has correctly observed, however, this is not “some unforeseen byproduct of a statutory scheme. ” Harper & Row, 471 U. S., at 589 (dissenting opinion). It is, rather, “the essence of copyright,” ibid., and a constitutional requirement. The primary objective of copyright is not to reward the labor of authors, but “[t]o promote the Progress of Science and useful Arts.” Art. I, §8, cl. 8. Accord, Twentieth Century Music Corp. v. Aiken, 422 U. S. 151, 156 (1975). To this end, copyright assures authors the right to their original expression, but encourages others to build freely upon the ideas and information conveyed by a work. Harper & Row, supra, at 556-557. This principle, known as the idea/ expression or fact/expression dichotomy, applies to all works of authorship. As applied to a factual compilation, assuming the absence of original written expression, only the compiler’s selection and arrangement may be protected; the raw facts may be copied at will. This result is neither unfair nor unfortunate. It is the means by which copyright advances the progress of science and art.\nThis Court has long recognized that the fact/expression dichotomy limits severely the scope of protection in fact-based works. More than a century ago, the Court observed: “The very object of publishing a book on science or the useful arts is to communicate to the world the useful knowledge which it contains. But this object would be frustrated if the knowledge could not be used without incurring the guilt of piracy of the book.” Baker v. Selden, 101 U. S. 99, 103 (1880). We reiterated this point in Harper & Row:\n“[N]o author may copyright facts or ideas. The copyright is limited to those aspects of the work — termed ‘expression’ — that display the stamp of the author’s originality.\n“[CJopyright does not prevent subsequent users from copying from a prior author’s work those constituent elements that are not original — for example . . . facts, or materials in the public domain — as long as such use does not unfairly appropriate the author’s original contributions.” 471 U. S., at 547-548 (citation omitted).\nThis, then, resolves the doctrinal tension: Copyright treats facts and factual compilations in a wholly consistent manner. Facts, whether alone or as part of a compilation, are not original and therefore may not be copyrighted. A factual compilation is eligible for copyright if it features an original selection or arrangement of facts, but the copyright is limited to the particular selection or arrangement. In no event may-copyright extend to the facts themselves.\nB\nAs we have explained, originality is a constitutionally mandated prerequisite for copyright protection. The Court’s decisions announcing this rule predate the Copyright Act of 1909, but ambiguous language in the 1909 Act caused some lower courts temporarily to lose sight of this requirement.\nThe 1909 Act embodied the originality requirement, but not as clearly as it might have. See Nimmer §2.01. The subject matter of copyright was set out in §§3 and 4 of the Act. Section 4 stated that copyright was available to “all the writings of an author.” 35 Stat. 1076. By using the words “writings” and “author” — the same words used in Article I, §8, of the Constitution and defined by the Court in The Trade-Mark Cases and Burrow-Giles — the statute necessarily incorporated the originality requirement articulated in the Court’s decisions. It did so implicitly, however, thereby leaving room for error.\nSection 3 was similarly ámbiguous. It stated that the copyright in a work protected only “the copyrightable component parts of the work.” It thus stated an important copyright principle, but failed to identify the specific characteristic — originality—that determined which component parts of a work were copyrightable and which were not.\nMost courts construed the 1909 Act correctly, notwithstanding the less-than-perfect statutory language. They understood from this Court’s decisions that there could be no copyright without originality. See Patterson & Joyce 760-761. As explained in the Nimmer treatise: “The 1909 Act neither defined originality, nor even expressly required that a work be ‘original’ in order to command protection. However, the courts uniformly inferred the requirement from the fact that copyright protection may only be claimed by ‘authors’ .... It was reasoned that since an author is ‘the . . . creator, originator’ it follows that a work is not the product of an author unless the work is original.” Nimmer § 2.01 (footnotes omitted) (citing cases).\nBut some courts misunderstood the statute. See, e. g., Leon v. Pacific Telephone & Telegraph Co., 91 F. 2d 484 (CA9 1937); Jeweler’s Circular Publishing Co. v. Keystone Publishing Co., 281 F. 83 (CA2 1922). These courts ignored §§ 3 and 4, focusing their attention instead on § 5 of the Act. Section 5, however, was purely technical in nature: It provided that a person seeking to register a work should indicate on the application the type of work, and it listed 14 categories under which the work might fall. One of these categories was “[b]ooks, including composite and cyclopaedic works, directories, gazetteers, and other compilations.” §5(a). Section 5 did not purport to say that all compilations were automatically copyrightable. Indeed, it expressly disclaimed any such function, pointing out that “the subject-matter of copyright [i]s defined in section four.” Nevertheless, the fact that factual compilations were mentioned specifically in § 5 led some courts to infer erroneously that directories and the like were copyrightable per se, “without any further or precise showing of original — personal—authorship.” Ginsburg 1895.\nMaking matters worse, these courts developed a new theory to justify the protection of factual compilations. Known alternatively as “sweat of the brow” or “industrious collection,” the underlying notion was that copyright was a reward for the hard work that went into compiling facts. The classic formulation of the doctrine appeared in Jeweler’s Circular Publishing Co., 281 F., at 88:\n“The right to copyright a book upon which one has expended labor in its preparation does not depend upon whether the materials which he has collected consist or not of matters which are publici juris, or whether such materials show literary skill or originality, either in thought or in language, or anything more than industrious collection. The man who goes through the streets of a town and puts down the names of each of the inhabitants, with their occupations and their street number, acquires material of which he is the author” (emphasis added).\nThe “sweat of the brow” doctrine had numerous flaws, the most glaring being that it extended copyright protection in a compilation beyond selection and arrangement — the compiler’s original contributions — to the facts themselves. Under the doctrine, the only defense to infringement was independent creation. A subsequent compiler was “not entitled to take one word of information previously published,” but rather had to “independently wor[k] out the matter for himself, so as to arrive at the same result from the same common sources of information.” Id., at 88-89 (internal quotation marks omitted). “Sweat of the brow” courts thereby eschewed the most fundamental axiom of copyright law — that no one may copyright facts or ideas. See Miller v. Universal City Studios, Inc., 650 F. 2d, at 1372 (criticizing “sweat of the brow” courts because “ensuring] that later writers obtain the facts independently ... is precisely the scope of protection given . . . copyrighted matter, and the law is clear that facts are not entitled to such protection”).\nDecisions of this Court applying the 1909 Act make clear that the statute did not permit the “sweat of the brow” approach. The best example is International News Service v. Associated Press, 248 U. S. 215 (1918). In that decision, the Court stated unambiguously that the 1909 Act conferred copyright protection only on those elements of a work that were original to the author. International News Service had conceded taking news reported by Associated Press and publishing it in its own newspapers. Recognizing that § 5 of the Act specifically mentioned “‘periodicals, including newspapers,’” § 5(b), the Court acknowledged that news articles were copyrightable. Id., at 234. It flatly rejected, however, the notion that the copyright in an article extended to the factual information it contained: “[T]he news element — the information respecting current events contained in the literary production — is not the creation of the writer, but is a report of matters that ordinarily are publici juris; it is the history of the day.” Ibid\n*\nWithout a doubt, the “sweat of the brow” doctrine flouted basic copyright principles. Throughout history, copyright law has “reeognize[d] a greater need to disseminate factual works than works of fiction or fantasy.” Harper & Row, 471 U. S., at 563. Accord, Gorman, Fact or Fancy: The Implications for Copyright, 29 J. Copyright Soc. 560, 563 (1982). But “sweat of the brow” courts took a contrary view; they handed out proprietary interests in facts and declared that authors are absolutely precluded from saving time and effort by relying upon the facts contained in prior works. In truth, “[i]t is just such wasted effort that the proscription against the copyright of ideas and facts . . . [is] designed to prevent.” Rosemont Enterprises, Inc. v. Random House, Inc., 366 F. 2d 303, 310 (CA2 1966), cert. denied, 385 U. S. 1009 (1967). “Protection for the fruits of such research . . . may in certain circumstances be available under a theory of unfair competition. But to accord copyright protection on this basis alone distorts basic copyright principles in that it creates a monopoly in public domain materials without the necessary justification of protecting and encouraging the creation of ‘writings’ by ‘authors.’” Nimmer §3.04, p. 3-23 (footnote omitted).\nC\n“Sweat of the brow” decisions did not escape the attention of the Copyright Office. When Congress decided to overhaul the copyright statute and asked the Copyright Office to study existing problems, see Mills Music, Inc. v. Snyder, 469 U. S. 153, 159 (1985), the Copyright Office promptly recommended that Congress clear up the confusion in the lower courts as to the basic standards of copyrightability. The Register of Copyrights explained in his first report to Congress that “originality” was a “basic requisite]” of copyright under the 1909 Act, but that “the absence of any reference to [originality] in the statute seems to have led to misconceptions as to what is copyrightable matter.” Report of the Register of Copyrights on the General Revision of the U. S. Copyright Law, 87th Cong., 1st Sess., p. 9 (H. Judiciary Comm. Print 1961). The Register suggested making the originality requirement explicit. Ibid.\nCongress took the Register’s advice. In enacting the Copyright Act of 1976, Congress dropped the reference to “all the writings of an author” and replaced it with the phrase “original works of authorship.” 17 U. S. C. § 102(a). In making explicit the originality requirement, Congress announced that it was merely clarifying existing law: “The two fundamental criteria of copyright protection [are] originality and fixation in tangible form .... The phrase ‘original works of authorship,’ which is purposely left undefined, is intended to incorporate without change the standard of originality established by the courts under the present [1909] copyright statute.” H. R. Rep. No. 94-1476, p. 51 (1976) (emphasis added) (hereinafter H. R. Rep.); S. Rep. No. 94-473, p. 50 (1975) (emphasis added) (hereinafter S. Rep.). This sentiment was echoed by the Copyright Office: “Our intention here is to maintain the established standards of originality . . . .” Supplementary Report of the Register of Copyrights on the General Revision of U. S. Copyright Law, 89th Cong., 1st Sess., pt. 6, p. 3 (H. Judiciary Comm. Print 1965) (emphasis added).\nTo ensure that the mistakes of the “sweat of the brow” courts would not be repeated, Congress took additional measures. For example, § 3 of the 1909 Act had stated that copyright protected only the “copyrightable component parts” of a work, but had not identified originality as the basis for distinguishing those component parts that were copyrightable from those that were not. The 1976 Act deleted this section and replaced it with § 102(b), which identifies specifically those elements of a work for which copyright is not available: “In no case does copyright protection for an original work of authorship extend to any idea, procedure, process, system, method of operation, concept, principle, or discovery, regardless of the form in which it is described, explained, illustrated, or embodied in such work.” Section 102(b) is universally understood to prohibit any copyright in facts. Harper & Row, supra, at 547, 556. Accord, Nimmer §2.03[E] (equating facts with “discoveries”). As with § 102(a), Congress emphasized that § 102(b) did not change the law, but merely clarified it: “Section 102(b) in no way enlarges or contracts the scope of copyright protection under the present law. Its purpose is to restate . . . that the basic dichotomy between expression and idea remains unchanged.” H. R. Rep., at 57; S. Rep., at 54.\nCongress took another step to minimize confusion by deleting the specific mention of “directories . . . and other compilations” in § 5 of the 1909 Act. As mentioned, this section had led some courts to conclude that directories were copyrightable per se and that every element of a directory was protected. In its place, Congress enacted two new provisions. First, to make clear that compilations were not copyrightable per se, Congress provided a definition of the term “compilation.” Second, to make clear that the copyright in a compilation did not extend to the facts themselves, Congress enacted § 103.\nThe definition of “compilation” is found in § 101 of the 1976 Act. It defines a “compilation” in the copyright sense as “a work formed by the collection and assembling of preexisting materials or of data that are selected, coordinated, or arranged in such a way that the resulting work as a whole constitutes an original work of authorship” (emphasis added).\nThe purpose of the statutory definition is to emphasize that collections of facts are not copyrightable per se. It conveys this message through its tripartite structure, as emphasized above by the italics. The statute identifies three distinct elements and requires each to be met for a work to qualify as a copyrightable compilation: (1) the collection and assembly of pre-existing material, facts, or data; (2) the selection, coordination, or arrangement of those materials; and (3) the creation, by virtue of the particular selection, coordination, or arrangement, of an “original” work of authorship. “[T]his tripartite conjunctive structure is self-evident, and should be assumed to ‘accurately express the legislative purpose.’” Patry 51, quoting Mills Music, 469 U. S., at 164.\nAt first glance, the first requirement does not seem to tell us much. It merely describes what one normally thinks of as a compilation — a collection of pre-existing material, facts, or data. What makes it significant is that it is not the sole requirement. It is not enough for copyright purposes that an author collects and assembles facts. To satisfy the statutory definition, the work must get over two additional hurdles. In this way, the plain language indicates that not every collection of facts receives copyright protection. Otherwise, there would be a period after “data.”\nThe third requirement is also illuminating. It emphasizes that a compilation, like any other work, is copyrightable only if it satisfies the originality requirement (“an original work of authorship”). Although § 102 states plainly that the originality requirement applies to all works, the point was emphasized with regard to compilations to ensure that courts would not repeat the mistake of the “sweat of the brow” courts by concluding that fact-based works are treated differently and measured by some other standard. As Congress explained it, the goal was to “make plain that the criteria of copyrightable subject matter stated in section 102 apply with full force to works . . . containing preexisting material.” H. R. Rep., at 57; S. Rep., at 55.\nThe key to the statutory definition is the second requirement. It instructs courts that, in determining whether a fact-based work is an original work of authorship, they should focus on the manner in which the collected facts have been selected, coordinated, and arranged. This is a straightforward application of the originality requirement. Facts are never original, so the compilation author can claim originality, if at all, only in the way the facts are presented. To that end, the statute dictates that the principal focus should be on whether the selection, coordination, and arrangement are sufficiently original to merit protection.\nNot every selection, coordination, or arrangement will pass muster. This is plain from the statute. It states that, to merit protection, the facts must be selected, coordinated, or arranged “in such a way” as to render the work as a whole original. This implies that some “ways” will trigger copyright, but that others will not. See Patry 57, and n. 76. Otherwise, the phrase “in such a way” is meaningless and Congress should have defined “compilation” simply as “a work formed by the collection and assembly of preexisting materials or data that are selected, coordinated, or arranged.” That Congress did not do so is dispositive. In accordance with “the established principle that a court should give effect, if possible, to every clause and word of a statute,” Moskal v. United States, 498 U. S. 103, 109-110 (1990) (internal quotation marks omitted), we conclude that the statute envisions that there will be some fact-based works in which the selection, coordination, and arrangement are not sufficiently original to trigger copyright protection.\nAs discussed earlier, however, the originality requirement is not particularly stringent. A compiler may settle upon a selection or arrangement that others have used; novelty is not required. Originality requires only that the author make the selection or arrangement independently (i. e., without copying that selection or arrangement from another work), and that it display some minimal level of creativity. Presumably, the vast majority of compilations will pass this test, but not all will. There remains a narrow category of works in which the creative spark is utterly lacking or so trivial as to be virtually nonexistent. See generally Bleistein v. Donaldson Lithographing Co., 188 U. S. 239, 251 (1903) (referring to “the narrowest and most obvious limits”). Such works are incapable of sustaining a valid copyright. Nimmer §2.01[B].\nEven if a work qualifies as a copyrightable compilation, it receives only limited protection. This is the point of § 103 of the Act. Section 103 explains that “[t]he subject matter of copyright. . . includes compilations,” § 103(a), but that copyright protects only the author’s original contributions — not the facts or information conveyed:\n“The copyright in a compilation . . . extends only to the material contributed by the author of such work, as distinguished from the preexisting material employed in the work, and does not imply any exclusive right in the preexisting material.” § 103(b).\nAs § 103 makes clear, copyright is not a tool by which a compilation author may keep others from using the facts or data he or she has collected. “The most important point here is one that is commonly misunderstood today: copyright . . . has no effect one way or the other on the copyright or public domain status of the preexisting material.” H. R. Rep., at 57; S. Rep., at 55. The 1909 Act did not require, as “sweat of the brow” courts mistakenly assumed, that each subsequent compiler must start from scratch and is precluded from relying on research undertaken by another. See, e. g., Jeweler’s Circular Publishing Co., 281 F., at 88-89. Rather, the facts contained in existing works may be freely copied because copyright protects only the elements that owe their origin to the compiler — the selection, coordination, and arrangement of facts.\nIn summary, the 1976 revisions to the Copyright Act leave no doubt that originality, not “sweat of the brow,” is the touchstone of copyright protection in directories and other fact-based works. Nor is there any doubt that the same was true under the 1909 Act. The 1976 revisions were a direct response to the Copyright Office’s concern that many lower courts had misconstrued this basic principle, and Congress emphasized repeatedly that the purpose of the revisions was to clarify, not change, existing law. The revisions explain with painstaking clarity that copyright requires originality, § 102(a); that facts are never original, § 102(b); that the copyright in a compilation does not extend to the facts it contains, § 103(b); and that a compilation is copyrightable only to the extent that it features an original selection, coordination, or arrangement, §101.\nThe 1976 revisions have proven largely successful in steering courts in the right direction. A good example is Miller v. Universal City Studios, Inc., 650 P. 2d, at 1369-1370: “A copyright in a directory ... is properly viewed as resting on the originality of the selection and arrangement of the factual material, rather than on the industriousness of the efforts to develop the information. Copyright protection does not extend to the facts themselves, and the mere use of information contained in a directory without a substantial copying of the format does not constitute infringement” (citation omitted). Additionally, the Second Circuit, which almost 70 years ago issued the classic formulation of the “sweat of the brow” doctrine in Jeweler’s Circular Publishing Co., has now fully repudiated the reasoning of that decision. See, e. g., Financial Information, Inc. v. Moody’s Investors Service, Inc., 808 F. 2d 204, 207 (CA2 1986), cert. denied, 484 U. S. 820 (1987); Financial Information, Inc. v. Moody’s Investors Service, Inc., 751 F. 2d 501, 510 (CA2 1984) (Newman, J., concurring); Hoehling v. Universal City Studios, Inc., 618 F. 2d 972, 979 (CA2 1980). Even those scholars who believe that “industrious collection” should be rewarded seem to recognize that this is beyond the scope of existing copyright law. See Denicola 516 (“[T]he very vocabulary of copyright is ill suited to analyzing property rights in works of nonfiction”); id., at 520-521, 525; Ginsburg 1867, 1870.\nb — ) b-H\nThere is no doubt that Feist took from the white pages of Rural’s directory a substantial amount of factual information. At a minimum, Feist copied the names, towns, and telephone numbers of 1,309 of Rural’s subscribers. Not all copying, however, is copyright infringement. To establish infringement, two elements must be proven: (1) ownership of a valid copyright, and (2) copying of constituent elements of the work that are original. See Harper & Row, 471 U. S., at 548. The first element is not at issue here; Feist appears to concede that Rural’s directory, considered as a whole, is subject to a valid copyright because it contains some foreword text, as well as original material in its yellow pages advertisements. See Brief for Petitioner 18; Pet. for Cert. 9.\nThe question is whether Rural has proved the second element. In other words, did Feist, by taking 1,309 names, towns, and telephone numbers from Rural’s white pages, copy anything that was “original” to Rural? Certainly, the raw data does not satisfy the originality requirement. Rural may have been the first to discover and report the names, towns, and telephone numbers of its subscribers, but this data does not K‘ow[e] its origin’” to Rural. Burrow-Giles, 111 U. S., at 58. Rather, these bits of information are uncopyrightable facts; they existed before Rural reported them and would have continued to exist if Rural had never published a telephone directory. The originality requirement “rule[s] out protecting . . . names, addresses, and telephone numbers of which the plaintiff by no stretch of the imagination could be called the author.” Patterson & Joyce 776.\nRural essentially concedes the point by referring to the names, towns, and telephone numbers as “preexisting material.” Brief for Respondent 17. Section 103(b) states explicitly that the copyright in a compilation does not extend to “the preexisting material employed in the work.”\nThe question that remains is whether Rural selected, coordinated, or arranged these uncopyrightable facts in an original way. As mentioned, originality is not a stringent standard; it does not require that facts be presented in an innovative or surprising way. It is equally true, however, that the selection and arrangement of facts cannot be so mechanical or routine as to require no creativity whatsoever. The standard of originality is low, but it does exist. See Patterson & Joyce 760, n. 144 (“While this requirement is sometimes characterized as modest, or a low threshold, it is not without effect”) (internal quotation marks omitted; citations omitted). As this Court has explained, the Constitution mandates some minimal degree of creativity, see The Trade-Mark Cases, 100 U. S., at 94; and an author who claims infringement must prove “the existence of . . . intellectual production, of thought, and conception.” Burrow-Giles, supra, at 69-60.\nThe selection, coordination, and arrangement of Rural’s white pages do not satisfy the minimum constitutional standards for copyright protection. As mentioned at the outset, Rural’s white pages are entirely typical. Persons desiring telephone service in Rural’s service area fill out an application and Rural issues them a telephone number. In preparing its white pages, Rural simply takes the data provided by its subscribers and lists it alphabetically by surname. The end product is a garden-variety white pages directory, devoid of even the slightest trace of creativity.\nRural’s selection of listings could not be more obvious: It publishes the most basic information — name, town, and telephone number — about each person who applies to it for telephone service. This is “selection” of a sort, but it lacks the modicum of creativity necessary to transform mere selection into copyrightable expression. Rural expended sufficient effort to make the white pages directory useful, but insufficient creativity to make it original.\nWe note in passing that the selection featured in Rural’s white pages may also fail the originality requirement for another reason. Feist points out that Rural did not truly “select” to publish the names and telephone numbers of its subscribers; rather, it was required to do so by the Kansas Corporation Commission as part of its monopoly franchise. See 737 F. Supp., at 612. Accordingly, one could plausibly conclude that this selection was dictated by state law, not by Rural.\nNor can Rural claim originality in its coordination and arrangement of facts. The white pages do nothing more than list Rural’s subscribers in alphabetical order. This arrangement may, technically speaking, owe its origin to Rural; no one disputes that Rural undertook the task of alphabetizing the names itself. But there is nothing remotely creative about arranging names alphabetically in a white pages directory. It is an age-old practice, firmly rooted in tradition and so commonplace that it has come to be expected as a matter of course. See Brief for Information Industry Association et al. as Amici Curiae 10 (alphabetical arrangement “is universally observed in directories published by local exchange telephone companies”). It is not only unoriginal, it is practically inevitable. This time-honored tradition does not possess the minimal creative spark required by the Copyright Act and the Constitution.\nWe conclude that the names, towns, and telephone numbers copied by Feist were not original to Rural and therefore were not protected by the copyright in Rural’s combined white and yellow pages directory. As a constitutional matter, copyright protects only those constituent elements of a work that possess more than a de minimis quantum of creativity. Rural’s white pages, limited to basic subscriber information and arranged alphabetically, fall short of the mark. As a statutory matter, 17 U. S. C. § 101 does not afford protection from copying to a collection of facts that are selected, coordinated, and arranged in a way that utterly lacks originality. Given that some works must fail, we cannot imagine a more likely candidate. Indeed, were we to hold that Rural’s white pages pass muster, it is hard to believe that any collection of facts could fail.\nBecause Rural’s white pages lack the requisite originality, Feist’s use of the listings cannot constitute infringement. This decision should not be construed as demeaning Rural’s efforts in compiling its directory, but rather as making clear that copyright rewards originality, not effort. As this Court noted more than a century ago, “ 'great praise may be due to the plaintiffs for their industry and enterprise in publishing this paper, yet the law does not contemplate their being rewarded in this way.’” Baker v. Selden, 101 U. S., at 105.\nThe judgment of the Court of Appeals is\nReversed.\nJustice Blackmun concurs in the judgment.\n* The Court ultimately rendered judgment for Associated Press on non-copyright grounds that are not relevant here. See 248 U. S., at 235, 241-242.",
+                "author": "Justice O’Connor",
+                "type": "majority"
+            }],
+            "judges": ["O’Connor, J., delivered the opinion of the Court, in which Rehnquist, C. J., and White, Marshall, Stevens, Scalia, Kennedy, and Sou-ter, JJ., joined. Blackmun, J., concurred in the judgment."],
+            "head_matter": "FEIST PUBLICATIONS, INC. v. RURAL TELEPHONE SERVICE CO., INC.\nNo. 89-1909.\nArgued January 9, 1991\nDecided March 27, 1991\nO’Connor, J., delivered the opinion of the Court, in which Rehnquist, C. J., and White, Marshall, Stevens, Scalia, Kennedy, and Sou-ter, JJ., joined. Blackmun, J., concurred in the judgment.\nKyler Knobbe argued the cause and filed briefs for petitioner.\nJames M. Caplinger, Jr., argued the cause and filed a brief for respondent.\n*\n'\n* Briefs of amici curiae urging reversal were filed for the Association of North American Directory Publishers et al. by Theodore Case Whitehouse; for the International Association of Cross Reference Directory Publishers by Richard D. Grauer and Kathleen McCree Lewis; and for the Third-Class Mail Association by Ian D. Volner.\nBriefs of amici curiae urging affirmance were filed for Ameritech et al. by Michael K. Kellogg, Charles Rothfeld, Douglas J. Kirk, Thomas P. Hester, and Harlan Sherwat; for the Association of American Publishers, Inc., by Robert G. Sugarman and R. Bruce Rich; for GTE Corp. by Kirk K. Van Tine, Richard M. Cahill, and Edivard R. Sublett; for the National Telephone Cooperative Association by L. Marie Guillory and David Cosson; for the United States Telephone Association by Richard J. Rappaport and Keith P. Schoeneberger; and for West Publishing Co. by Vance K. Opperman and James E. Schatz.\nBriefs of amici curiae were filed for BellSouth Corp. by Anthony B. Askew, Robert E. Richards, Walter H. Alford, and Vincent L. Sgrosso; for the Direct Marketing Association, Inc., by Robert L. Sherman; for Haines and Co., Inc., by Jeremiah D. McAuliffe, Bernard A. Barken, and Eugene Gressman; and for the Information Industry Association et al. by Steven J. Metalitz and Angela Burnett."
+        },
+        "status": "ok"
+    }
+}
\ No newline at end of file
diff --git a/notebooks/introduction.ipynb b/notebooks/introduction.ipynb
index 8f4cf614..b7bc059c 100644
--- a/notebooks/introduction.ipynb
+++ b/notebooks/introduction.ipynb
@@ -554,7 +554,7 @@
       "GIVEN: fact <the Lotus menu command hierarchy> was copyrightable\n",
       "GIVEN: fact <Borland International> copied constituent elements of <the Lotus menu command hierarchy> that were original\n",
       "according to the legislation\n",
-      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device. \" (Title 17, /us/usc/t17/s102/a)\n",
+      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "\n"
      ]
     }
@@ -636,7 +636,7 @@
       "GIVEN: fact <Dracula> was copyrightable\n",
       "GIVEN: fact <Prana Film> copied constituent elements of <Dracula> that were original\n",
       "according to the legislation\n",
-      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device. \" (Title 17, /us/usc/t17/s102/a)\n",
+      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "\n"
      ]
     }
@@ -921,7 +921,7 @@
       "DESPITE: fact a text described <the Lotus menu command hierarchy>\n",
       "DESPITE: fact <the Lotus menu command hierarchy> was an original work\n",
       "according to the legislation\n",
-      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any \" (Title 17, /us/usc/t17/s102/b)\n",
+      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any\" (Title 17, /us/usc/t17/s102/b)\n",
       "GIVEN: \"method of operation\" (Title 17, /us/usc/t17/s102/b)\n",
       "\n"
      ]
@@ -1039,7 +1039,7 @@
       "and despite the legislation\n",
       "DESPITE: \"In no case does copyright protection for an original work of authorship extend to any\" (Title 17, /us/usc/t17/s102/b)\n",
       "DESPITE: \"method of operation\" (Title 17, /us/usc/t17/s102/b)\n",
-      "DESPITE: \"The following are examples of works not subject to copyright and applications for registration of such works cannot be entertained: (a) Words and short phrases such as names, titles, and slogans; \" (Code of Federal Regulations Title 37, /us/cfr/t37/s202.1)\n",
+      "DESPITE: \"The following are examples of works not subject to copyright and applications for registration of such works cannot be entertained: (a) Words and short phrases such as names, titles, and slogans;\" (Code of Federal Regulations Title 37, /us/cfr/t37/s202.1)\n",
       "\n"
      ]
     }
@@ -1140,21 +1140,21 @@
       "GIVEN: fact <the Lotus menu command hierarchy> was copyrightable\n",
       "GIVEN: fact <Borland International> copied constituent elements of <the Lotus menu command hierarchy> that were original\n",
       "according to the legislation\n",
-      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device. \" (Title 17, /us/usc/t17/s102/a)\n",
+      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "\n",
-      "the rule that the court MAY ALWAYS accept the result\n",
+      "the rule that the court MUST ALWAYS accept the result\n",
       "RESULT: absence of fact <Borland International> infringed the copyright in <the Lotus menu command hierarchy>\n",
       "based on the input\n",
       "GIVEN: absence of fact <the Lotus menu command hierarchy> was copyrightable\n",
       "according to the legislation\n",
-      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device. \" (Title 17, /us/usc/t17/s102/a)\n",
+      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "\n",
-      "the rule that the court MAY ALWAYS accept the result\n",
+      "the rule that the court MUST ALWAYS accept the result\n",
       "RESULT: absence of fact <Borland International> infringed the copyright in <the Lotus menu command hierarchy>\n",
       "based on the input\n",
       "GIVEN: absence of fact <Borland International> copied constituent elements of <the Lotus menu command hierarchy> that were original\n",
       "according to the legislation\n",
-      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device. \" (Title 17, /us/usc/t17/s102/a)\n",
+      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "\n"
      ]
     }
@@ -1217,7 +1217,7 @@
       "based on the input\n",
       "GIVEN: evidence which supports fact <Borland International> copied <the Lotus menu command hierarchy> in creating <Quattro's Lotus Emulation Interface>\n",
       "according to the legislation\n",
-      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device. \" (Title 17, /us/usc/t17/s102/a)\n",
+      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "\n"
      ]
     }
@@ -1249,7 +1249,7 @@
       "GIVEN: fact <Borland International> published <Quattro's Lotus Emulation Interface>\n",
       "GIVEN: evidence which supports fact <Quattro's Lotus Emulation Interface> was very similar to <the Lotus menu command hierarchy>\n",
       "according to the legislation\n",
-      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device. \" (Title 17, /us/usc/t17/s102/a)\n",
+      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "\n"
      ]
     }
@@ -1280,7 +1280,7 @@
       "GIVEN: fact <Borland International> copied <the Lotus menu command hierarchy> in creating <Quattro's Lotus Emulation Interface>\n",
       "GIVEN: fact the copying of <Quattro's Lotus Emulation Interface> in <the Lotus menu command hierarchy> was so extensive that it rendered them substantially similar\n",
       "according to the legislation\n",
-      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device. \" (Title 17, /us/usc/t17/s102/a)\n",
+      "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "\n"
      ]
     }
@@ -1312,7 +1312,7 @@
       "DESPITE: fact a text described <the Lotus menu command hierarchy>\n",
       "DESPITE: fact <the Lotus menu command hierarchy> was an original work\n",
       "according to the legislation\n",
-      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any \" (Title 17, /us/usc/t17/s102/b)\n",
+      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any\" (Title 17, /us/usc/t17/s102/b)\n",
       "GIVEN: \"method of operation\" (Title 17, /us/usc/t17/s102/b)\n",
       "\n"
      ]
@@ -1347,7 +1347,7 @@
       "GIVEN: fact for another computer program to by operated in substantially the same way as <Lotus 1-2-3>, the other program would have to copy <the Lotus menu command hierarchy>\n",
       "DESPITE: fact the developers of <Lotus 1-2-3> made some expressive choices in choosing and arranging the terms in <the Lotus menu command hierarchy>\n",
       "according to the legislation\n",
-      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any \" (Title 17, /us/usc/t17/s102/b)\n",
+      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any\" (Title 17, /us/usc/t17/s102/b)\n",
       "GIVEN: \"method of operation\" (Title 17, /us/usc/t17/s102/b)\n",
       "\n"
      ]
@@ -1379,7 +1379,7 @@
       "GIVEN: fact <the Lotus menu command hierarchy> was the means by which a person operated <Lotus 1-2-3>\n",
       "DESPITE: fact it is false that <the Lotus menu command hierarchy> was an abstraction\n",
       "according to the legislation\n",
-      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any \" (Title 17, /us/usc/t17/s102/b)\n",
+      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any\" (Title 17, /us/usc/t17/s102/b)\n",
       "GIVEN: \"method of operation\" (Title 17, /us/usc/t17/s102/b)\n",
       "\n"
      ]
@@ -1412,7 +1412,7 @@
       "GIVEN: fact it is false that the precise formulation of <Lotus 1-2-3>'s code was necessary for it to work\n",
       "DESPITE: fact computer code was necessary for <Lotus 1-2-3> to work\n",
       "according to the legislation\n",
-      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any \" (Title 17, /us/usc/t17/s102/b)\n",
+      "GIVEN: \"In no case does copyright protection for an original work of authorship extend to any\" (Title 17, /us/usc/t17/s102/b)\n",
       "GIVEN: \"method of operation\" (Title 17, /us/usc/t17/s102/b)\n",
       "\n"
      ]
@@ -1727,7 +1727,7 @@
       "and despite the legislation\n",
       "DESPITE: \"In no case does copyright protection for an original work of authorship extend to any\" (Title 17, /us/usc/t17/s102/b)\n",
       "DESPITE: \"method of operation\" (Title 17, /us/usc/t17/s102/b)\n",
-      "DESPITE: \"The following are examples of works not subject to copyright and applications for registration of such works cannot be entertained: (a) Words and short phrases such as names, titles, and slogans; \" (Code of Federal Regulations Title 37, /us/cfr/t37/s202.1)\n",
+      "DESPITE: \"The following are examples of works not subject to copyright and applications for registration of such works cannot be entertained: (a) Words and short phrases such as names, titles, and slogans;\" (Code of Federal Regulations Title 37, /us/cfr/t37/s202.1)\n",
       "\n"
      ]
     }
@@ -1884,7 +1884,7 @@
       "GIVEN: \"Copyright protection subsists, in accordance with this title, in original works of authorship fixed in any tangible medium of expression, now known or later developed, from which they can be perceived, reproduced, or otherwise communicated, either directly or with the aid of a machine or device.\" (Title 17, /us/usc/t17/s102/a)\n",
       "and despite the legislation\n",
       "DESPITE: \"In no case does copyright protection for an original work of authorship extend to any idea, procedure, process, system, method of operation, concept, principle, or discovery, regardless of the form in which it is described, explained, illustrated, or embodied in such work.\" (Title 17, /us/usc/t17/s102/b)\n",
-      "DESPITE: \"The following are examples of works not subject to copyright and applications for registration of such works cannot be entertained: (a) Words and short phrases such as names, titles, and slogans; \" (Code of Federal Regulations Title 37, /us/cfr/t37/s202.1)\n",
+      "DESPITE: \"The following are examples of works not subject to copyright and applications for registration of such works cannot be entertained: (a) Words and short phrases such as names, titles, and slogans;\" (Code of Federal Regulations Title 37, /us/cfr/t37/s202.1)\n",
       "\n"
      ]
     }
diff --git a/readme.md b/readme.md
index 98cf0b96..07171aac 100644
--- a/readme.md
+++ b/readme.md
@@ -12,17 +12,14 @@ Use this button to interact with the notebook online through Binder.
 
 [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/mscarey/AuthoritySpoke/master)
 
-[Full documentation for AuthoritySpoke](https://authorityspoke.readthedocs.io/en/latest/) is a work in progress.
+You can find AuthoritySpoke's [API documentation and more at ReadTheDocs](https://authorityspoke.readthedocs.io/en/latest/).
 
 ## Installing AuthoritySpoke
 
-I suggest running AuthoritySpoke in a Pipenv environment. See the [installation instructions for Pipenv](https://docs.pipenv.org/en/latest/install/#installing-pipenv).
-
-Then run
+AuthoritySpoke is now [available on PyPI](https://pypi.org/project/AuthoritySpoke/), so you can install it with pip:
 
 ```
-$ pipenv install
-$ pipenv shell
+$ pip install authorityspoke
 ```
 
 If everything worked, AuthoritySpoke should be available to import in your Python environment.
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 15d6a3ca..4486271f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -12,6 +12,7 @@
 
 black
 coveralls
+ipykernel
 mypy
 pipenv-to-requirements
 pydocstyle
@@ -25,4 +26,3 @@ sphinx
 sphinx-autodoc-typehints
 twine
 wheel
-wily
diff --git a/requirements-docs.txt b/requirements-docs.txt
index f8e1e17a..6d477e2b 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,7 +1,5 @@
 -e .
 beautifulsoup4>=4.7.1
-ipykernel
-lxml
 pint>=0.9
 requests
 sphinx>=2.0.1
diff --git a/requirements.txt b/requirements.txt
index 2a1730da..73335481 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,6 @@
 
 -e .
 beautifulsoup4>=4.7.1
-ipykernel
 lxml
 pint>=0.9
 requests
diff --git a/setup.py b/setup.py
index 1122f7be..a4fa6bb5 100644
--- a/setup.py
+++ b/setup.py
@@ -15,8 +15,8 @@
     packages=setuptools.find_packages(
         exclude=["tests", "*.tests", "*.tests.*", "tests.*"]
     ),
-    install_requires=["beautifulsoup4", "lxml", "pint", "requests", "ipykernel"],
-    extras_require={"jupyter notebooks": "jupyter"},
+    install_requires=["beautifulsoup4", "lxml", "pint", "requests"],
+    extras_require={"jupyter notebooks": ["jupyter", "ipykernel"]},
     classifiers=[
         "Development Status :: 3 - Alpha",
         "Intended Audience :: Legal Industry",


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 9dac9043..cc2a12ca 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,7 +10,7 @@
 from authorityspoke.jurisdictions import Jurisdiction, Regime
 from authorityspoke.opinions import Opinion
 from authorityspoke.predicates import Predicate, Q_
-from authorityspoke.rules import Procedure, ProceduralRule, Rule
+from authorityspoke.rules import Procedure, Rule
 from authorityspoke.selectors import TextQuoteSelector
 
 
@@ -231,6 +231,7 @@ def watt_factor(make_predicate, make_entity, watt_mentioned) -> Dict[str, Factor
         "f2_entity_order": Fact(p["p2"], case_factors=c),
         "f2_no_truth": Fact(p["p2_no_truth"], (1, 0), case_factors=c),
         "f2_false": Fact(p["p2_false"], case_factors=c),
+        "f2_false_absent": Fact(p["p2_false"], absent=True, case_factors=c),
         "f2_reciprocal": Fact(p["p2_reciprocal"], case_factors=c),
         "f2_generic": Fact(p["p2"], generic=True, case_factors=c),
         "f2_false_generic": Fact(p["p2_false"], generic=True, case_factors=c),
@@ -434,6 +435,21 @@ def make_exhibit(
         "testimony_no_statement": Exhibit(form="testimony"),
     }
 
+@pytest.fixture(scope="class")
+def make_complex_rule(make_factor, make_exhibit, make_complex_fact) -> Dict[str, Rule]:
+    return {"accept_relevance_testimony": Rule(
+        inputs=make_exhibit["relevant_murder_testimony"],
+        outputs=make_complex_fact["f_relevant_murder"]
+    ),
+    "accept_relevance_testimony_ALL": Rule(
+        inputs=make_exhibit["relevant_murder_testimony"],
+        outputs=make_complex_fact["f_relevant_murder"],
+        universal=True
+    ),
+    "accept_murder_fact_from_relevance": Rule(
+        inputs=make_complex_fact["f_relevant_murder"],
+        outputs=make_factor["f_murder"]
+    )}
 
 @pytest.fixture(scope="class")
 def make_evidence(
@@ -481,11 +497,14 @@ def make_evidence(
     }
 
 @pytest.fixture(scope="module")
-def make_selector() -> Dict[str, TextQuoteSelector]:
+def make_selector(make_code) -> Dict[str, TextQuoteSelector]:
     return {"/us/usc/t17/s103": TextQuoteSelector(
             path = "/us/usc/t17/s103",
             exact = "protection for a work employing preexisting material in which copyright subsists does not extend to any part of the work in which such material has been used unlawfully."
-    )}
+    ),
+    "copyright": TextQuoteSelector(
+            path="/us/usc/t17/s102/b", suffix="idea, procedure,", source=make_code["usc17"]
+        )}
 
 @pytest.fixture(scope="module")
 def make_regime() -> Dict[str, Code]:
@@ -515,8 +534,11 @@ def make_code(make_regime) -> Dict[str, Code]:
 
 
 @pytest.fixture(scope="module")
-def make_enactment(make_regime) -> Dict[str, Enactment]:
+def make_enactment(make_selector, make_regime) -> Dict[str, Enactment]:
     return {
+        "copyright": Enactment(
+            selector=make_selector["copyright"], regime=make_regime
+        ),
         "search_clause": Enactment(
             selector=TextQuoteSelector(
                 path="/us/const/amendment-IV",
@@ -529,6 +551,13 @@ def make_enactment(make_regime) -> Dict[str, Enactment]:
             ),
             regime=make_regime
             ),
+        "warrants_clause": Enactment(
+            selector=TextQuoteSelector(
+                path="/us/const/amendment-IV",
+                exact="shall not be violated, and no Warrants shall issue,",
+            ),
+            regime=make_regime
+        ),
         "fourth_a": Enactment(
             selector=TextQuoteSelector(
             path="/us/const/amendment-IV",
@@ -716,7 +745,7 @@ def make_procedure(make_evidence, make_factor, watt_factor) -> Dict[str, Procedu
 
 
 @pytest.fixture(scope="class")
-def real_holding(make_procedure, make_enactment) -> Dict[str, ProceduralRule]:
+def real_holding(make_procedure, make_enactment) -> Dict[str, Rule]:
     """These holdings can be changed in case they don't accurately reflect
     what's in real cases, or in case there are API improvements that
     allow them to become more accurate. I'll try not to write any tests
@@ -726,255 +755,255 @@ def real_holding(make_procedure, make_enactment) -> Dict[str, ProceduralRule]:
     e = make_enactment
 
     return {
-        "h1": ProceduralRule(c["c1"], enactments=e["search_clause"], mandatory=True),
-        "h2": ProceduralRule(c["c2"], enactments=e["search_clause"], mandatory=True),
-        "h3": ProceduralRule(c["c3"], enactments=e["search_clause"], mandatory=True),
-        "h4": ProceduralRule(c["c4"], enactments=e["search_clause"], mandatory=True),
+        "h1": Rule(c["c1"], enactments=e["search_clause"], mandatory=True),
+        "h2": Rule(c["c2"], enactments=e["search_clause"], mandatory=True),
+        "h3": Rule(c["c3"], enactments=e["search_clause"], mandatory=True),
+        "h4": Rule(c["c4"], enactments=e["search_clause"], mandatory=True),
     }
 
 
 @pytest.fixture(scope="class")
-def make_holding(make_procedure, make_enactment) -> Dict[str, ProceduralRule]:
+def make_holding(make_procedure, make_enactment) -> Dict[str, Rule]:
     c = make_procedure
     e = make_enactment
 
     return {
-        "h1": ProceduralRule(c["c1"], enactments=e["search_clause"]),
-        "h2": ProceduralRule(c["c2"], enactments=e["search_clause"]),
-        "h3": ProceduralRule(c["c3"], enactments=e["search_clause"]),
-        "h1_again": ProceduralRule(c["c1"], enactments=e["search_clause"]),
-        "h1_entity_order": ProceduralRule(
+        "h1": Rule(c["c1"], enactments=e["search_clause"]),
+        "h2": Rule(c["c2"], enactments=e["search_clause"]),
+        "h3": Rule(c["c3"], enactments=e["search_clause"]),
+        "h1_again": Rule(c["c1"], enactments=e["search_clause"]),
+        "h1_entity_order": Rule(
             c["c1_entity_order"], enactments=e["search_clause"]
         ),
-        "h1_easy": ProceduralRule(c["c1_easy"], enactments=e["search_clause"]),
-        "h1_opposite": ProceduralRule(
+        "h1_easy": Rule(c["c1_easy"], enactments=e["search_clause"]),
+        "h1_opposite": Rule(
             c["c1"], enactments=e["search_clause"], rule_valid=False
         ),
-        "h2_without_cite": ProceduralRule(c["c2"]),
-        "h2_fourth_a_cite": ProceduralRule(c["c2"], enactments=e["fourth_a"]),
-        "h2_despite_due_process": ProceduralRule(
+        "h2_without_cite": Rule(c["c2"]),
+        "h2_fourth_a_cite": Rule(c["c2"], enactments=e["fourth_a"]),
+        "h2_despite_due_process": Rule(
             c["c2"],
             enactments=e["search_clause"],
             enactments_despite=e["due_process_5"],
         ),
-        "h2_ALL_due_process": ProceduralRule(
+        "h2_ALL_due_process": Rule(
             c["c2"],
             enactments=(e["search_clause"], e["due_process_5"]),
             mandatory=False,
             universal=True,
             rule_valid=True,
         ),
-        "h2_ALL_due_process_invalid": ProceduralRule(
+        "h2_ALL_due_process_invalid": Rule(
             c["c2"],
             enactments=(e["search_clause"], e["due_process_5"]),
             mandatory=False,
             universal=True,
             rule_valid=False,
         ),
-        "h2_ALL": ProceduralRule(
+        "h2_ALL": Rule(
             c["c2"], enactments=e["search_clause"], mandatory=False, universal=True
         ),
-        "h2_ALL_invalid": ProceduralRule(
+        "h2_ALL_invalid": Rule(
             c["c2"],
             enactments=e["search_clause"],
             mandatory=False,
             universal=True,
             rule_valid=False,
         ),
-        "h2_ALL_MAY_output_false": ProceduralRule(
+        "h2_ALL_MAY_output_false": Rule(
             c["c2_output_false"],
             enactments=e["search_clause"],
             mandatory=False,
             universal=True,
         ),
-        "h2_ALL_MUST": ProceduralRule(
+        "h2_ALL_MUST": Rule(
             c["c2"], enactments=e["search_clause"], mandatory=True, universal=True
         ),
-        "h2_ALL_MUST_output_false": ProceduralRule(
+        "h2_ALL_MUST_output_false": Rule(
             c["c2_output_false"],
             enactments=e["search_clause"],
             mandatory=True,
             universal=True,
         ),
-        "h2_exact_quantity": ProceduralRule(
+        "h2_exact_quantity": Rule(
             c["c2_exact_quantity"], enactments=e["search_clause"]
         ),
-        "h2_invalid": ProceduralRule(
+        "h2_invalid": Rule(
             c["c2"], enactments=e["search_clause"], rule_valid=False
         ),
-        "h2_irrelevant_inputs": ProceduralRule(
+        "h2_irrelevant_inputs": Rule(
             c["c2_irrelevant_inputs"], enactments=e["search_clause"]
         ),
-        "h2_irrelevant_inputs_invalid": ProceduralRule(
+        "h2_irrelevant_inputs_invalid": Rule(
             c["c2_irrelevant_inputs"], enactments=e["search_clause"], rule_valid=False
         ),
-        "h2_irrelevant_inputs_ALL_MUST": ProceduralRule(
+        "h2_irrelevant_inputs_ALL_MUST": Rule(
             c["c2_irrelevant_inputs"],
             enactments=e["search_clause"],
             mandatory=True,
             universal=True,
         ),
-        "h2_irrelevant_inputs_ALL_MUST_invalid": ProceduralRule(
+        "h2_irrelevant_inputs_ALL_MUST_invalid": Rule(
             c["c2_irrelevant_inputs"],
             enactments=e["search_clause"],
             mandatory=True,
             universal=True,
             rule_valid=False,
         ),
-        "h2_irrelevant_inputs_ALL_invalid": ProceduralRule(
+        "h2_irrelevant_inputs_ALL_invalid": Rule(
             c["c2_irrelevant_inputs"],
             enactments=e["search_clause"],
             universal=True,
             rule_valid=False,
         ),
-        "h2_irrelevant_inputs_MUST": ProceduralRule(
+        "h2_irrelevant_inputs_MUST": Rule(
             c["c2_irrelevant_inputs"], enactments=e["search_clause"], mandatory=True
         ),
-        "h2_irrelevant_inputs_MUST_invalid": ProceduralRule(
+        "h2_irrelevant_inputs_MUST_invalid": Rule(
             c["c2_irrelevant_inputs"],
             enactments=e["search_clause"],
             mandatory=True,
             rule_valid=False,
         ),
-        "h2_reciprocal_swap": ProceduralRule(
+        "h2_reciprocal_swap": Rule(
             c["c2_reciprocal_swap"], enactments=e["search_clause"]
         ),
-        "h2_exact_in_despite": ProceduralRule(
+        "h2_exact_in_despite": Rule(
             c["c2_exact_in_despite"], enactments=e["search_clause"]
         ),
-        "h2_exact_in_despite_ALL": ProceduralRule(
+        "h2_exact_in_despite_ALL": Rule(
             c["c2_exact_in_despite"],
             enactments=e["search_clause"],
             mandatory=False,
             universal=True,
         ),
-        "h2_exact_in_despite_ALL_entity_order": ProceduralRule(
+        "h2_exact_in_despite_ALL_entity_order": Rule(
             c["c2_exact_in_despite_entity_order"],
             enactments=e["search_clause"],
             mandatory=False,
             universal=True,
         ),
-        "h2_exact_quantity_ALL": ProceduralRule(
+        "h2_exact_quantity_ALL": Rule(
             c["c2_exact_quantity"],
             enactments=e["search_clause"],
             mandatory=False,
             universal=True,
         ),
-        "h2_invalid_undecided": ProceduralRule(
+        "h2_invalid_undecided": Rule(
             c["c2"], enactments=e["search_clause"], rule_valid=False, decided=False
         ),
-        "h2_MUST": ProceduralRule(
+        "h2_MUST": Rule(
             c["c2"], enactments=e["search_clause"], mandatory=True, universal=False
         ),
-        "h2_MUST_invalid": ProceduralRule(
+        "h2_MUST_invalid": Rule(
             c["c2"], enactments=e["search_clause"], mandatory=True, rule_valid=False
         ),
-        "h2_output_absent": ProceduralRule(
+        "h2_output_absent": Rule(
             c["c2_output_absent"], enactments=e["search_clause"]
         ),
-        "h2_output_false": ProceduralRule(
+        "h2_output_false": Rule(
             c["c2_output_false"], enactments=e["search_clause"]
         ),
-        "h2_output_false_ALL": ProceduralRule(
+        "h2_output_false_ALL": Rule(
             c["c2_output_false"], enactments=e["search_clause"], universal=True
         ),
-        "h2_output_absent_false": ProceduralRule(
+        "h2_output_absent_false": Rule(
             c["c2_output_absent_false"], enactments=e["search_clause"]
         ),
-        "h2_SOME_MUST_output_false": ProceduralRule(
+        "h2_SOME_MUST_output_false": Rule(
             c["c2_output_false"],
             enactments=e["search_clause"],
             mandatory=True,
             universal=False,
         ),
-        "h2_SOME_MUST_output_absent": ProceduralRule(
+        "h2_SOME_MUST_output_absent": Rule(
             c["c2_output_absent"],
             enactments=e["search_clause"],
             mandatory=True,
             universal=False,
         ),
-        "h2_undecided": ProceduralRule(
+        "h2_undecided": Rule(
             c["c2"], enactments=e["search_clause"], decided=False
         ),
-        "h2_irrelevant_inputs_undecided": ProceduralRule(
+        "h2_irrelevant_inputs_undecided": Rule(
             c["c2_irrelevant_inputs"], enactments=e["search_clause"], decided=False
         ),
-        "h2_MUST_undecided": ProceduralRule(
+        "h2_MUST_undecided": Rule(
             c["c2"], enactments=e["search_clause"], mandatory=True, decided=False
         ),
-        "h3_ALL": ProceduralRule(
+        "h3_ALL": Rule(
             c["c3"], enactments=e["search_clause"], universal=True
         ),
-        "h3_fewer_inputs": ProceduralRule(
+        "h3_fewer_inputs": Rule(
             c["c3_fewer_inputs"], enactments=e["search_clause"]
         ),
-        "h3_undecided": ProceduralRule(
+        "h3_undecided": Rule(
             c["c3"], enactments=e["search_clause"], decided=False
         ),
-        "h3_ALL_undecided": ProceduralRule(
+        "h3_ALL_undecided": Rule(
             c["c3"], enactments=e["search_clause"], decided=False, universal=True
         ),
-        "h3_fewer_inputs_ALL": ProceduralRule(
+        "h3_fewer_inputs_ALL": Rule(
             c["c3_fewer_inputs"], enactments=e["search_clause"], universal=True
         ),
-        "h3_fewer_inputs_undecided": ProceduralRule(
+        "h3_fewer_inputs_undecided": Rule(
             c["c3_fewer_inputs"], enactments=e["search_clause"], decided=False
         ),
-        "h3_fewer_inputs_ALL_undecided": ProceduralRule(
+        "h3_fewer_inputs_ALL_undecided": Rule(
             c["c3_fewer_inputs"],
             enactments=e["search_clause"],
             universal=True,
             decided=False,
         ),
-        "h_near_means_curtilage": ProceduralRule(
+        "h_near_means_curtilage": Rule(
             c["c_near_means_curtilage"], enactments=e["search_clause"]
         ),
-        "h_near_means_curtilage_even_if": ProceduralRule(
+        "h_near_means_curtilage_even_if": Rule(
             c["c_near_means_curtilage_even_if"], enactments=e["search_clause"]
         ),
-        "h_near_means_curtilage_ALL_MUST": ProceduralRule(
+        "h_near_means_curtilage_ALL_MUST": Rule(
             c["c_near_means_curtilage"],
             enactments=e["search_clause"],
             mandatory=True,
             universal=True,
         ),
-        "h_near_means_curtilage_ALL_undecided": ProceduralRule(
+        "h_near_means_curtilage_ALL_undecided": Rule(
             c["c_near_means_curtilage"],
             enactments=e["search_clause"],
             universal=True,
             decided=False,
         ),
-        "h_near_means_no_curtilage": ProceduralRule(
+        "h_near_means_no_curtilage": Rule(
             c["c_near_means_no_curtilage"], enactments=e["search_clause"]
         ),
-        "h_near_means_no_curtilage_ALL": ProceduralRule(
+        "h_near_means_no_curtilage_ALL": Rule(
             c["c_near_means_no_curtilage"],
             enactments=e["search_clause"],
             universal=True,
         ),
-        "h_near_means_no_curtilage_ALL_MUST": ProceduralRule(
+        "h_near_means_no_curtilage_ALL_MUST": Rule(
             c["c_near_means_no_curtilage"],
             enactments=e["search_clause"],
             mandatory=True,
             universal=True,
         ),
-        "h_nearer_means_curtilage": ProceduralRule(
+        "h_nearer_means_curtilage": Rule(
             c["c_nearer_means_curtilage"], enactments=e["search_clause"]
         ),
-        "h_nearer_means_curtilage_ALL": ProceduralRule(
+        "h_nearer_means_curtilage_ALL": Rule(
             c["c_nearer_means_curtilage"], enactments=e["search_clause"], universal=True
         ),
-        "h_nearer_means_curtilage_MUST": ProceduralRule(
+        "h_nearer_means_curtilage_MUST": Rule(
             c["c_nearer_means_curtilage"], enactments=e["search_clause"], mandatory=True
         ),
-        "h_far_means_no_curtilage": ProceduralRule(c["c_far_means_no_curtilage"]),
-        "h_far_means_no_curtilage_ALL": ProceduralRule(
+        "h_far_means_no_curtilage": Rule(c["c_far_means_no_curtilage"]),
+        "h_far_means_no_curtilage_ALL": Rule(
             c["c_far_means_no_curtilage"], enactments=e["search_clause"], universal=True
         ),
-        "h_output_distance_less": ProceduralRule(
+        "h_output_distance_less": Rule(
             c["c_output_distance_less"], universal=True, mandatory=True
         ),
-        "h_output_distance_more": ProceduralRule(c["c_output_distance_more"]),
+        "h_output_distance_more": Rule(c["c_output_distance_more"]),
     }
 
 
@@ -992,7 +1021,7 @@ def make_opinion(make_entity) -> Dict[str, Opinion]:
 
 @pytest.fixture(scope="class")
 def make_opinion_with_holding(make_regime) -> Dict[str, Opinion]:
-    test_cases = ("brad", "cardenas", "lotus", "watt", "oracle")
+    test_cases = ("brad", "cardenas", "feist", "lotus", "watt", "oracle")
     opinions = {}
     for case in test_cases:
         opinion = Opinion.from_file(f"{case}_h.json")
diff --git a/tests/test_download.py b/tests/test_download.py
index 2ac90293..aec65b89 100644
--- a/tests/test_download.py
+++ b/tests/test_download.py
@@ -4,7 +4,7 @@
 
 from authorityspoke.opinions import Opinion
 
-# pytestmark = pytest.mark.skip("don't want to call API")
+pytestmark = pytest.mark.skip("don't want to call API")
 
 
 class TestDownload:
diff --git a/tests/test_enactments.py b/tests/test_enactments.py
index 62f722aa..8ae3040f 100644
--- a/tests/test_enactments.py
+++ b/tests/test_enactments.py
@@ -183,3 +183,57 @@ def test_compare_effective_dates(self, make_enactment):
         dp5 = make_enactment["due_process_5"]
         dp14 = make_enactment["due_process_14"]
         assert dp14.effective_date > dp5.effective_date
+
+    @pytest.mark.parametrize(
+        "enactment_name, text",
+        [
+            ("due_process_5", "life, liberty, or property, without due process of law"),
+            (
+                "copyright",
+                "In no case does copyright protection for an original work of authorship extend to any",
+            ),
+        ],
+    )
+    def test_text_interval(self, make_enactment, enactment_name, text):
+        """
+        The interval represents the start and end of the phrase
+        "life, liberty, or property, without due process of law"
+        in the Fifth Amendment.
+        """
+        provision = make_enactment[enactment_name]
+        interval = provision.text_interval()
+        this_section = provision.code.section_text(path=provision.selector.path)
+        assert this_section[interval[0] : interval[1]] == text
+
+    # Addition
+
+    def test_add_overlapping_enactments(self, make_enactment):
+        search_clause = make_enactment["search_clause"]
+        warrant_clause = make_enactment["warrants_clause"]
+
+        combined = search_clause + warrant_clause
+
+        passage = (
+            "against unreasonable searches and seizures, "
+            + "shall not be violated, "
+            + "and no Warrants shall issue,"
+        )
+        assert passage in combined.text
+
+    def test_add_shorter_plus_longer(self, make_enactment):
+        search_clause = make_enactment["search_clause"]
+        fourth_a = make_enactment["fourth_a"]
+
+        combined = search_clause + fourth_a
+
+        assert combined.text == fourth_a.text
+        assert combined == fourth_a
+
+    def test_add_longer_plus_shorter(self, make_enactment):
+        search_clause = make_enactment["search_clause"]
+        fourth_a = make_enactment["fourth_a"]
+
+        combined = fourth_a + search_clause
+
+        assert combined.text == fourth_a.text
+        assert combined == fourth_a
diff --git a/tests/test_entities.py b/tests/test_entities.py
index 3b7f79e1..20e2f5f2 100644
--- a/tests/test_entities.py
+++ b/tests/test_entities.py
@@ -86,6 +86,14 @@ def test_implication_subclass(self, make_entity):
         assert make_entity["tree_search_specific"] >= make_entity["motel"]
         assert make_entity["tree_search"] > make_entity["motel"]
 
+    def test_plural_true(self, make_opinion_with_holding):
+        """
+        holding_feist.json has an entity with the name "Rural's telephone listings"
+        and "plural": true
+        """
+        feist = make_opinion_with_holding["feist_majority"]
+        assert any(entity.plural is True for entity in feist.generic_factors)
+
     # Contradiction
 
     def test_error_contradiction_with_non_factor(self, make_entity, make_predicate):
diff --git a/tests/test_factors.py b/tests/test_factors.py
index 59a9f3f5..7b33fccc 100644
--- a/tests/test_factors.py
+++ b/tests/test_factors.py
@@ -4,9 +4,9 @@
 import pytest
 
 from authorityspoke.factors import Entity, Factor, Fact, means
-from authorityspoke.rules import Rule, ProceduralRule
+from authorityspoke.rules import Rule
 from authorityspoke.opinions import Opinion
-from authorityspoke.predicates import ureg, Q_
+from authorityspoke.predicates import ureg, Q_, Predicate
 
 
 class TestFacts:
@@ -354,6 +354,13 @@ def test_standard_of_proof_inequality(self, watt_factor):
         assert not f["f2_clear_and_convincing"].means(f["f2_preponderance_of_evidence"])
         assert not f["f2_clear_and_convincing"].means(f["f2"])
 
+    def test_means_despite_plural(self):
+        directory = Entity("Rural's telephone directory", plural=False)
+        listings = Entity("Rural's telephone listings", plural=True)
+        directory_original = Fact(Predicate("{} was original"), context_factors=directory)
+        listings_original = Fact(Predicate("{} were original"), context_factors=listings)
+        assert directory_original.means(listings_original)
+
     # Implication
 
     def test_specific_factor_implies_generic(self, watt_factor):
@@ -473,6 +480,16 @@ def test_no_contradiction_complex(self, make_complex_fact):
     def test_no_contradiction_of_None(self, watt_factor):
         assert not watt_factor["f1"].contradicts(None)
 
+    def test_contradicts_if_present_both_present(self, watt_factor):
+        """
+        Test a helper function that checks whether there would
+        be a contradiction if neither Factor was "absent".
+        """
+        assert watt_factor["f2"]._contradicts_if_present(watt_factor["f2_false"])
+
+    def test_contradicts_if_present_one_absent(self, watt_factor):
+        assert watt_factor["f2"]._contradicts_if_present(watt_factor["f2_false_absent"])
+
     # Consistency with Entity/Factor assignments
 
     def test_copy_with_foreign_context(self, watt_mentioned, watt_factor):
@@ -532,3 +549,14 @@ def test_check_entity_consistency_type_error(
         )
         with pytest.raises(TypeError):
             any(register is not None for register in update)
+
+    # Addition
+
+    @pytest.mark.parametrize("left, right, expected",
+    [
+        ("f_shooting_craig_poe", "f_shooting_craig_brd", "f_shooting_craig_brd"),
+        ("f_irrelevant_3", "f_irrelevant_3_new_context", "f_irrelevant_3"),
+        ("f_irrelevant_3_new_context", "f_irrelevant_3", "f_irrelevant_3_new_context"),
+    ])
+    def test_addition(self, make_factor, left, right, expected):
+        assert make_factor[left] + make_factor[right] == make_factor[expected]
diff --git a/tests/test_io.py b/tests/test_io.py
index c7856808..3b8d9dfe 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -11,6 +11,7 @@
 from authorityspoke.factors import Factor, Fact
 from authorityspoke.opinions import Opinion
 from authorityspoke.predicates import Predicate
+from authorityspoke.procedures import Procedure
 from authorityspoke.rules import Rule
 
 ureg = pint.UnitRegistry()
@@ -31,7 +32,6 @@ def test_import_predicate_with_quantity(self):
 
 
 class TestEntityImport:
-
     def test_specific_entity(self):
         smith_dict = {
             "mentioned_factors": [
@@ -264,6 +264,69 @@ def test_error_classname_does_not_exist(self):
         with pytest.raises(ValueError):
             Rule.collection_from_dict(rule_dict)
 
+    def test_holding_flagged_exclusive(self, make_opinion_with_holding):
+        """
+        Test that "exclusive" flag doesn't mess up the holding where it's placed.
+
+        Test whether the Feist opinion object includes a holding
+        with the output "Rural's telephone directory
+        was copyrightable" and the input "Rural's telephone
+        directory was original", when that holding was marked
+        "exclusive" in the JSON.
+        """
+
+        directory = Entity("Rural's telephone directory")
+        original = Fact(Predicate("{} was original"), directory)
+        copyrightable = Fact(Predicate("{} was copyrightable"), directory)
+        originality_procedure = Procedure(outputs=copyrightable, inputs=original)
+        originality_rule = Rule(originality_procedure, mandatory=False, universal=False)
+        assert any(
+            feist_rule.means(originality_rule)
+            for feist_rule in make_opinion_with_holding["feist_majority"].holdings
+        )
+
+    def test_holding_inferred_from_exclusive(self, make_opinion_with_holding):
+        """
+        Test whether the Feist opinion object includes a holding
+        that was inferred from an entry in the JSON saying that the
+        "exclusive" way to reach the output "Rural's telephone directory
+        was copyrightable" is to have the input "Rural's telephone
+        directory was original".
+
+        The inferred holding says that in the absence of the input
+        "Rural's telephone directory was original", the court MUST
+        ALWAYS find the output to be absent as well.
+        """
+
+        directory = Entity("Rural's telephone directory")
+        not_original = Fact(Predicate("{} was original"), directory, absent=True)
+        not_copyrightable = Fact(
+            Predicate("{} was copyrightable"), directory, absent=True
+        )
+        no_originality_procedure = Procedure(
+            outputs=not_copyrightable, inputs=not_original
+        )
+        no_originality_rule = Rule(
+            no_originality_procedure, mandatory=True, universal=True
+        )
+        feist = make_opinion_with_holding["feist_majority"]
+        assert feist.holdings[4].means(no_originality_rule)
+
+    def test_exclusive_results_in_more_holdings(self, make_opinion_with_holding):
+        """
+        Test that a holding inferred due to the "exclusive" flag
+        is created in addition to all the other holdings, so there
+        is one more holding than there are entries in the "holdings"
+        section of the JSON.
+        """
+
+        with open(Rule.directory / "holding_feist.json", "r") as f:
+            feist_json = json.load(f)
+        assert (
+            len(make_opinion_with_holding["feist_majority"].holdings)
+            == len(feist_json["holdings"]) + 1
+        )
+
 
 class TestNestedFactorImport:
     def test_import_holding(self, make_regime):
diff --git a/tests/test_notebooks.py b/tests/test_notebooks.py
index de271f35..87de0c26 100644
--- a/tests/test_notebooks.py
+++ b/tests/test_notebooks.py
@@ -27,6 +27,17 @@ def test_replace_generic_factor(self, make_opinion_with_holding):
         assert lotus_majority.holdings[0] != nosferatu_rule
         assert lotus_majority.holdings[0].means(nosferatu_rule)
 
+    def test_inferred_holdings_after_exclusive_holding(self, make_opinion_with_holding):
+        """
+        Test that when a holding is marked "exclusive" in the JSON input,
+        that holding is added first to the Opinion's set of holdings, and
+        any other inferred holdings, about the absence of the output from
+        the original holding, are added later.
+        """
+        lotus_majority = make_opinion_with_holding["lotus_majority"]
+        assert lotus_majority.holdings[0].outputs[0].absent is False
+        assert lotus_majority.holdings[1].outputs[0].absent is True
+
     def test_evolve_rule_replacing_enactment(
         self, make_regime, make_opinion_with_holding
     ):
diff --git a/tests/test_opinions.py b/tests/test_opinions.py
index bb4ec9b1..3f7e3c78 100644
--- a/tests/test_opinions.py
+++ b/tests/test_opinions.py
@@ -3,7 +3,8 @@
 
 from authorityspoke.enactments import Code, Enactment
 from authorityspoke.factors import Entity, Evidence, Exhibit
-from authorityspoke.rules import Procedure, Rule, ProceduralRule
+from authorityspoke.procedures import Procedure
+from authorityspoke.rules import Rule
 from authorityspoke.opinions import Opinion
 from authorityspoke.predicates import ureg, Q_
 from authorityspoke.context import log_mentioned_context
diff --git a/tests/test_predicates.py b/tests/test_predicates.py
index 6eabd295..25ea6017 100644
--- a/tests/test_predicates.py
+++ b/tests/test_predicates.py
@@ -4,7 +4,7 @@
 import pytest
 
 from authorityspoke.factors import Entity, Factor, Fact
-from authorityspoke.rules import Rule, ProceduralRule
+from authorityspoke.rules import Rule
 from authorityspoke.opinions import Opinion
 from authorityspoke.predicates import Predicate, ureg, Q_
 
@@ -57,6 +57,30 @@ def test_str_for_predicate_with_number_quantity(self, make_predicate):
             make_predicate["p8"]
         )
 
+    @pytest.mark.parametrize(
+        "sentence, index, expected",
+        [
+            (
+                "{} was names, towns, and telephone numbers of telephone subscribers",
+                0,
+                "{} were names, towns,",
+            ),
+            (
+                "all of {} and {} was at the meeting",
+                0,
+                "all of {} and {} was at the meeting",
+            ),
+            (
+                "all of {} and {} was at the meeting",
+                1,
+                "all of {} and {} were at the meeting",
+            ),
+        ],
+    )
+    def test_make_str_plural(self, sentence, index, expected):
+        plural_version = Predicate.make_context_plural(sentence=sentence, index=index)
+        assert plural_version.startswith(expected)
+
     def test_negated_method(self, make_predicate):
         assert make_predicate["p7"].negated() == make_predicate["p7_opposite"]
         assert make_predicate["p3"].negated() == make_predicate["p3_false"]
diff --git a/tests/test_procedures.py b/tests/test_procedures.py
index 2fa4941f..5b4eda2a 100644
--- a/tests/test_procedures.py
+++ b/tests/test_procedures.py
@@ -4,7 +4,8 @@
 from authorityspoke.enactments import Code, Enactment
 from authorityspoke.factors import Predicate, Entity, Factor, Fact
 from authorityspoke.factors import Evidence, Exhibit
-from authorityspoke.rules import Procedure, Rule, ProceduralRule
+from authorityspoke.procedures import Procedure
+from authorityspoke.rules import Rule
 from authorityspoke.opinions import Opinion
 from authorityspoke.predicates import ureg, Q_
 from authorityspoke.context import log_mentioned_context
diff --git a/tests/test_rules.py b/tests/test_rules.py
index e31d934b..02915deb 100644
--- a/tests/test_rules.py
+++ b/tests/test_rules.py
@@ -4,7 +4,8 @@
 from authorityspoke.enactments import Code, Enactment
 from authorityspoke.factors import Predicate, Entity, Factor, Fact
 from authorityspoke.factors import Evidence, Exhibit
-from authorityspoke.rules import Procedure, Rule, ProceduralRule
+from authorityspoke.procedures import Procedure
+from authorityspoke.rules import Rule
 from authorityspoke.opinions import Opinion
 from authorityspoke.predicates import ureg, Q_
 from authorityspoke.context import log_mentioned_context
@@ -82,8 +83,15 @@ def test_factor_properties_for_rule(self, make_regime):
         assert len(cardenas_holdings[1].outputs) == 1
         assert len(cardenas_holdings[1].despite) == 1
 
-    # Same Meaning
+    def test_single_enactment_converted_to_tuple(self, make_holding):
+        assert isinstance(make_holding["h2"].enactments, tuple)
 
+    def test_holding_len(self, make_holding):
+        assert len(make_holding["h1"]) == 2
+        assert len(make_holding["h3"]) == 5
+
+
+class TestSameMeaning:
     def test_identical_holdings_equal(self, make_holding):
         assert make_holding["h1"].means(make_holding["h1_again"])
 
@@ -102,8 +110,11 @@ def test_holdings_different_entities_unequal(self, make_holding):
     def test_holdings_differing_in_entity_order_equal(self, make_holding):
         assert make_holding["h1"].means(make_holding["h1_entity_order"])
 
-    # Implication
+    def test_holdings_citing_different_enactment_text_unequal(self, make_holding):
+        assert make_holding["h2"] != make_holding["h2_fourth_a_cite"]
+
 
+class TestImplication:
     def test_holdings_more_inputs_implies_fewer(self, make_holding):
         assert make_holding["h1"] > make_holding["h1_easy"]
         assert make_holding["h2_irrelevant_inputs"] > make_holding["h2"]
@@ -220,8 +231,39 @@ def test_error_implication_with_procedure(self, make_holding, make_procedure):
         with pytest.raises(TypeError):
             make_holding["h2_undecided"] >= make_procedure["c2"]
 
-    # Contradiction
+    def test_implication_with_evidence(self, make_holding):
+        assert make_holding["h3"] > make_holding["h3_fewer_inputs"]
+
+    def test_holding_based_on_less_text_implies_more(self, make_holding):
+        """The implied holding is based on enactment text that is a superset
+        of the implying holding's enactment text."""
+        assert make_holding["h2"] > make_holding["h2_fourth_a_cite"]
+
+    def test_holding_with_enactment_cite_does_not_imply_without(self, make_holding):
+        """Just because an outcome is required by enactment text doesn't mean
+        the court would require it pursuant to its common law power to make laws."""
+        assert not make_holding["h2"] >= make_holding["h2_without_cite"]
+
+    def test_implication_common_law_and_constitutional(self, make_holding):
+        """When a court asserts a holding as valid without legislative support,
+        the court is actually making a broader statement than it would be making
+        if it cited legislative support. The holding without legislative support
+        doesn't depend for its validity on the enactment remaining in effect without
+        being repealed.
 
+        The relative priority of the holdings is a different
+        matter. Statutory holdings trump common law holdings, and constitutional
+        trumps statutory."""
+        assert make_holding["h2"] <= make_holding["h2_without_cite"]
+
+    def test_no_implication_of_holding_with_added_despite_enactment(self, make_holding):
+        assert not make_holding["h2"] >= make_holding["h2_despite_due_process"]
+
+    def test_implication_of_holding_with_removed_despite_enactment(self, make_holding):
+        assert make_holding["h2_despite_due_process"] >= make_holding["h2"]
+
+
+class TestContradiction:
     def test_error_contradiction_with_procedure(self, make_holding, make_procedure):
         with pytest.raises(TypeError):
             make_holding["h2_undecided"].contradicts(make_procedure["c2"])
@@ -566,41 +608,20 @@ def test_undecided_holding_no_implied_contradiction(self, make_holding):
             make_holding["h2_irrelevant_inputs_undecided"]
         )
 
-    # Enactments cited in Rules
-
-    def test_single_enactment_converted_to_frozenset(self, make_holding):
-        assert isinstance(make_holding["h2"].enactments, tuple)
-
-    def test_holdings_citing_different_enactment_text_unequal(self, make_holding):
-        assert make_holding["h2"] != make_holding["h2_fourth_a_cite"]
-
-    def test_holding_based_on_less_text_implies_more(self, make_holding):
-        """The implied holding is based on enactment text that is a superset
-        of the implying holding's enactment text."""
-        assert make_holding["h2"] > make_holding["h2_fourth_a_cite"]
-
-    def test_holding_with_enactment_cite_does_not_imply_without(self, make_holding):
-        """Just because an outcome is required by enactment text doesn't mean
-        the court would require it pursuant to its common law power to make laws."""
-        assert not make_holding["h2"] >= make_holding["h2_without_cite"]
-
-    def test_implication_common_law_and_constitutional(self, make_holding):
-        """When a court asserts a holding as valid without legislative support,
-        the court is actually making a broader statement than it would be making
-        if it cited legislative support. The holding without legislative support
-        doesn't depend for its validity on the enactment remaining in effect without
-        being repealed.
-
-        The relative priority of the holdings is a different
-        matter. Statutory holdings trump common law holdings, and constitutional
-        trumps statutory."""
-        assert make_holding["h2"] <= make_holding["h2_without_cite"]
+    def test_contradiction_with_evidence(self, make_holding):
+        assert make_holding["h3_ALL_undecided"].contradicts(
+            make_holding["h3_fewer_inputs_ALL"]
+        )
 
-    def test_no_implication_of_holding_with_added_despite_enactment(self, make_holding):
-        assert not make_holding["h2"] >= make_holding["h2_despite_due_process"]
+    def test_no_contradiction_same_undecided_holding(self, make_holding):
+        assert not make_holding["h3_ALL_undecided"].contradicts(
+            make_holding["h3_ALL_undecided"]
+        )
 
-    def test_implication_of_holding_with_removed_despite_enactment(self, make_holding):
-        assert make_holding["h2_despite_due_process"] >= make_holding["h2"]
+    def test_no_contradiction_holding_with_evidence(self, make_holding):
+        assert not make_holding["h3_fewer_inputs_ALL_undecided"].contradicts(
+            make_holding["h3_ALL"]
+        )
 
     def test_no_contradiction_when_added_enactment_makes_rule_valid(self, make_holding):
         assert not make_holding["h2_ALL_due_process"].contradicts(
@@ -617,24 +638,91 @@ def test_contradiction_with_fewer_enactments(self, make_holding):
             make_holding["h2_ALL"]
         )
 
-    def test_implication_with_evidence(self, make_holding):
-        assert make_holding["h3"] > make_holding["h3_fewer_inputs"]
 
-    def test_contradiction_with_evidence(self, make_holding):
-        assert make_holding["h3_ALL_undecided"].contradicts(
-            make_holding["h3_fewer_inputs_ALL"]
-        )
+class TestAddition:
+    def test_add_simple_rules(self):
+        """
+        A simple form of two Rules from Feist, with no Enactments.
 
-    def test_no_contradiction_same_undecided_holding(self, make_holding):
-        assert not make_holding["h3_ALL_undecided"].contradicts(
-            make_holding["h3_ALL_undecided"]
-        )
+        Even though the rules have different generic Factors (i.e. Entities),
+        the __add__ function will make one Rule using the generic Factors from
+        the operand on the left, but will give it the output from the operand
+        on the right.
+        """
+        context = Entity("the Pythagorean theorem")
+        three = Entity("the number three")
+
+        fact_not_original = Rule(
+            Procedure(
+                inputs=Fact(Predicate("{} is a fact"), context_factors=context),
+                outputs=Fact(
+                    Predicate("{} is original", truth=False), context_factors=context
+                ),
+            ),
+            universal=True,
+        )
+        unoriginal_not_copyrightable = Rule(
+            Procedure(
+                inputs=Fact(
+                    Predicate("{} is original", truth=False), context_factors=three
+                ),
+                outputs=Fact(
+                    Predicate("{} is copyrightable", truth=False), context_factors=three
+                ),
+            ),
+            universal=True,
+        )
+
+        facts_not_copyrightable = fact_not_original + unoriginal_not_copyrightable
+        assert len(facts_not_copyrightable.inputs) == 1
+        assert str(facts_not_copyrightable.inputs[0]) == (
+            "fact <the Pythagorean theorem> is a fact"
+        )
+        assert len(facts_not_copyrightable.outputs) == 2
+        assert str(facts_not_copyrightable.outputs[1]) == (
+            "fact it is false that <the Pythagorean theorem> is copyrightable"
+        )
+
+    def test_add_inferred_rule(self, make_opinion_with_holding):
+        """
+        test implication between
+        telephone listings -> not original (feist.holdings[11])
+        +
+        not original -> not copyrightable (feist.holdings[3])
+        =
+        telephone listings -> not copyrightable
 
-    def test_no_contradiction_holding_with_evidence(self, make_holding):
-        assert not make_holding["h3_fewer_inputs_ALL_undecided"].contradicts(
-            make_holding["h3_ALL"]
+        """
+        feist = make_opinion_with_holding["feist_majority"]
+        listings_not_original = feist.holdings[11]
+        unoriginal_not_copyrightable = feist.holdings[4]
+        listings_not_copyrightable = (
+            listings_not_original + unoriginal_not_copyrightable
+        )
+        assert len(listings_not_copyrightable.inputs) == 1
+        assert str(listings_not_copyrightable.inputs[0]) == (
+            "fact <Rural's telephone listings> were names, towns, "
+            + "and telephone numbers of telephone subscribers"
+        )
+        assert any(
+            str(out) == "absence of fact <Rural's telephone listings> were copyrightable"
+            for out in listings_not_copyrightable.outputs
         )
 
-    def test_holding_len(self, make_holding):
-        assert len(make_holding["h1"]) == 2
-        assert len(make_holding["h3"]) == 5
+    def test_add_some_plus_some_makes_none(self, make_complex_rule):
+        """The rules can't be added because they both have universal==False"""
+        new_rule = make_complex_rule["accept_relevance_testimony"] + make_complex_rule["accept_murder_fact_from_relevance"]
+        assert new_rule is None
+
+    def test_add_complex_rule(self, make_factor, make_complex_rule):
+        """
+        The resulting rule will have universal==False because one of the
+        two input rules has universal==False.
+        """
+        new_rule = make_complex_rule["accept_relevance_testimony_ALL"] + make_complex_rule["accept_murder_fact_from_relevance"]
+        assert new_rule.universal is False
+        assert new_rule.inputs == make_complex_rule["accept_relevance_testimony_ALL"].inputs
+        assert make_complex_rule["accept_murder_fact_from_relevance"].outputs[0] in new_rule.outputs
+
+
+    # Add tests that consider differences in Enactments
diff --git a/tests/test_selectors.py b/tests/test_selectors.py
index b0b158a6..b84dd9d9 100644
--- a/tests/test_selectors.py
+++ b/tests/test_selectors.py
@@ -3,7 +3,8 @@
 from authorityspoke.enactments import Code, Enactment
 from authorityspoke.factors import Predicate, Entity, Factor, Fact
 from authorityspoke.factors import Evidence, Exhibit
-from authorityspoke.rules import Procedure, Rule, ProceduralRule
+from authorityspoke.procedures import Procedure
+from authorityspoke.rules import Rule
 from authorityspoke.opinions import Opinion
 from authorityspoke.selectors import TextQuoteSelector
 
@@ -40,11 +41,8 @@ def test_add_omitted_initial_slash(self, make_code):
         )
         assert selector.path.startswith("/")
 
-    def test_passage_from_uslm_code(self, make_code):
-        usc17 = make_code["usc17"]
-        copyright_exceptions = TextQuoteSelector(
-            path="/us/usc/t17/s102/b", suffix="idea, procedure,", source=usc17
-        )
+    def test_passage_from_uslm_code(self, make_selector):
+        copyright_exceptions = make_selector["copyright"]
         assert copyright_exceptions.exact.strip() == (
             "In no case does copyright protection "
             + "for an original work of authorship extend to any"